init
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "java",
|
||||||
|
"name": "Spring Boot 실행",
|
||||||
|
"request": "launch",
|
||||||
|
"mainClass": "com.example.Application",
|
||||||
|
"projectName": "demo",
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"vmArgs": "-Dspring.profiles.active=local"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"java.compile.nullAnalysis.mode": "disabled"
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"java.compile.nullAnalysis.mode": "disabled",
|
||||||
|
"java.configuration.updateBuildConfiguration": "automatic"
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'java'
|
id 'java'
|
||||||
id 'org.springframework.boot' version '3.5.11'
|
id 'org.springframework.boot' version '3.5.11'
|
||||||
|
id 'io.spring.dependency-management' version '1.1.7'
|
||||||
}
|
}
|
||||||
|
|
||||||
group = 'kr.iotdoor'
|
group = 'kr.iotdoor'
|
||||||
@@ -22,17 +23,13 @@ ext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
|
|
||||||
runtimeOnly platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
|
|
||||||
annotationProcessor platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
|
|
||||||
testImplementation platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
|
|
||||||
|
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||||
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||||
|
|
||||||
implementation "org.mybatis.spring.boot:mybatis-spring-boot-starter:${mybatisSpringBootVersion}"
|
implementation "org.mybatis.spring.boot:mybatis-spring-boot-starter:${mybatisSpringBootVersion}"
|
||||||
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
|
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
|
||||||
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
|
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
|
||||||
@@ -51,7 +48,6 @@ tasks.withType(JavaCompile).configureEach {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.named('bootRun') {
|
tasks.named('bootRun') {
|
||||||
// Let bootRun read templates/static resources directly from src/main/resources.
|
|
||||||
sourceResources sourceSets.main
|
sourceResources sourceSets.main
|
||||||
systemProperty 'spring.devtools.restart.enabled', 'true'
|
systemProperty 'spring.devtools.restart.enabled', 'true'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
"@tiptap/core": "^3.12.0",
|
"@tiptap/core": "^3.12.0",
|
||||||
"@tiptap/extension-image": "^3.12.0",
|
"@tiptap/extension-image": "^3.12.0",
|
||||||
"@tiptap/pm": "^3.12.0",
|
"@tiptap/pm": "^3.12.0",
|
||||||
"@tiptap/starter-kit": "^3.12.0"
|
"@tiptap/starter-kit": "^3.12.0",
|
||||||
|
"ag-grid-community": "^35.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.25.9"
|
"esbuild": "^0.25.9"
|
||||||
@@ -815,6 +816,19 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||||
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="
|
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/ag-charts-types": {
|
||||||
|
"version": "13.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-13.2.1.tgz",
|
||||||
|
"integrity": "sha512-r7veb3QqJtIKlXmeUsLR4/oDPwmHxFI2tmbZra/203mdaz3uwQUrrgYNg628nrK+7L2YxXnwGc6L05tWjLLjNQ=="
|
||||||
|
},
|
||||||
|
"node_modules/ag-grid-community": {
|
||||||
|
"version": "35.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-35.2.1.tgz",
|
||||||
|
"integrity": "sha512-ycmGI+1EbUT7i3eg/Kgi1owwnkdHXRufo10Xm6cfSsVPM3TMpvlbLgi28KIPt9DGHZWHq9fOBn7nxMNdv1Yaow==",
|
||||||
|
"dependencies": {
|
||||||
|
"ag-charts-types": "13.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/argparse": {
|
"node_modules/argparse": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
|
|||||||
@@ -2,13 +2,16 @@
|
|||||||
"name": "iotdoor-admin-assets",
|
"name": "iotdoor-admin-assets",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:admin-js": "esbuild src/main/frontend/iotdoor.js --bundle --format=iife --global-name=IotdoorAdmin --target=es2019 --minify --outfile=src/main/resources/static/resources/kr/iotdoor/boot/admin/js/iotdoor.js"
|
"build:admin-js": "esbuild src/main/frontend/iotdoor.js --bundle --format=iife --global-name=IotdoorAdmin --target=es2019 --minify --outfile=src/main/resources/static/resources/kr/iotdoor/boot/admin/js/iotdoor.js",
|
||||||
|
"build:portfolio-public": "esbuild src/main/frontend/portfolio-public.js --bundle --format=iife --global-name=IotdoorPortfolio --target=es2019 --minify --outdir=src/main/resources/static/resources/kr/iotdoor/cmty/portfolio",
|
||||||
|
"build:assets": "npm run build:admin-js && npm run build:portfolio-public"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tiptap/core": "^3.12.0",
|
"@tiptap/core": "^3.12.0",
|
||||||
"@tiptap/extension-image": "^3.12.0",
|
"@tiptap/extension-image": "^3.12.0",
|
||||||
"@tiptap/pm": "^3.12.0",
|
"@tiptap/pm": "^3.12.0",
|
||||||
"@tiptap/starter-kit": "^3.12.0"
|
"@tiptap/starter-kit": "^3.12.0",
|
||||||
|
"ag-grid-community": "^35.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.25.9"
|
"esbuild": "^0.25.9"
|
||||||
|
|||||||
@@ -312,6 +312,104 @@ async function uploadImage(file, options, form) {
|
|||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isImageFile(file) {
|
||||||
|
if (!file) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.type?.startsWith("image/")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return /\.(avif|bmp|gif|jpe?g|png|svg|webp)$/i.test(file.name || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasFileTransfer(dataTransfer) {
|
||||||
|
if (!dataTransfer) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
Array.from(dataTransfer.types || []).includes("Files") ||
|
||||||
|
Array.from(dataTransfer.items || []).some((item) => item.kind === "file") ||
|
||||||
|
(dataTransfer.files?.length || 0) > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImageFiles(files) {
|
||||||
|
return Array.from(files || []).filter(isImageFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDropInsertPosition(editor, event) {
|
||||||
|
const position = editor.view.posAtCoords({
|
||||||
|
left: event.clientX,
|
||||||
|
top: event.clientY,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof position?.pos !== "number") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(0, Math.min(position.pos, editor.state.doc.content.size));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPortfolioImageNode(payload, file, isRepresentative) {
|
||||||
|
return {
|
||||||
|
type: PORTFOLIO_IMAGE_TYPE,
|
||||||
|
attrs: {
|
||||||
|
src: payload.url,
|
||||||
|
alt: payload.orgnlFileNm || file.name,
|
||||||
|
atchFileIdntfNo: payload.atchFileIdntfNo,
|
||||||
|
fileSn: payload.fileSn,
|
||||||
|
isRepresentative,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertImageNodes(editor, imageNodes, insertAt) {
|
||||||
|
if (!imageNodes.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chain = editor.chain().focus();
|
||||||
|
if (typeof insertAt === "number") {
|
||||||
|
chain.insertContentAt(insertAt, imageNodes).run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
chain.insertContent(imageNodes).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadAndInsertImages(files, editor, options, form, insertAt, editorElement) {
|
||||||
|
const imageFiles = getImageFiles(files);
|
||||||
|
|
||||||
|
if (!imageFiles.length) {
|
||||||
|
window.alert("이미지 파일만 업로드할 수 있습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
editorElement?.classList.add("is-uploading-image");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const imageNodes = [];
|
||||||
|
let shouldBeRepresentative = !getRepresentativeImage(editor);
|
||||||
|
|
||||||
|
for (const file of imageFiles) {
|
||||||
|
const payload = await uploadImage(file, options, form);
|
||||||
|
imageNodes.push(
|
||||||
|
createPortfolioImageNode(payload, file, shouldBeRepresentative),
|
||||||
|
);
|
||||||
|
shouldBeRepresentative = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
insertImageNodes(editor, imageNodes, insertAt);
|
||||||
|
} catch (error) {
|
||||||
|
window.alert(error.message || "이미지 업로드에 실패했습니다.");
|
||||||
|
} finally {
|
||||||
|
editorElement?.classList.remove("is-uploading-image");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function ensureLegacyRepresentative(editor, form) {
|
function ensureLegacyRepresentative(editor, form) {
|
||||||
const currentUrl = form.dataset.currentThumbnailUrl || "";
|
const currentUrl = form.dataset.currentThumbnailUrl || "";
|
||||||
const currentAtchFileIdntfNo =
|
const currentAtchFileIdntfNo =
|
||||||
@@ -442,6 +540,70 @@ function attachToolbarActions(editor, toolbar, fileInput) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function attachImageDropUpload(editor, editorElement, options, form) {
|
||||||
|
let dragDepth = 0;
|
||||||
|
|
||||||
|
const resetDragState = () => {
|
||||||
|
dragDepth = 0;
|
||||||
|
editorElement.classList.remove("is-dragging-image");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragEnter = (event) => {
|
||||||
|
if (!hasFileTransfer(event.dataTransfer)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dragDepth += 1;
|
||||||
|
editorElement.classList.add("is-dragging-image");
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (event) => {
|
||||||
|
if (!hasFileTransfer(event.dataTransfer)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.dataTransfer.dropEffect = "copy";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragLeave = (event) => {
|
||||||
|
if (!hasFileTransfer(event.dataTransfer)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dragDepth -= 1;
|
||||||
|
if (dragDepth <= 0 || !editorElement.contains(event.relatedTarget)) {
|
||||||
|
resetDragState();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (event) => {
|
||||||
|
if (!hasFileTransfer(event.dataTransfer)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
resetDragState();
|
||||||
|
|
||||||
|
const insertAt = getDropInsertPosition(editor, event);
|
||||||
|
void uploadAndInsertImages(
|
||||||
|
event.dataTransfer.files,
|
||||||
|
editor,
|
||||||
|
options,
|
||||||
|
form,
|
||||||
|
insertAt,
|
||||||
|
editorElement,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
editorElement.addEventListener("dragenter", handleDragEnter, true);
|
||||||
|
editorElement.addEventListener("dragover", handleDragOver, true);
|
||||||
|
editorElement.addEventListener("dragleave", handleDragLeave, true);
|
||||||
|
editorElement.addEventListener("drop", handleDrop, true);
|
||||||
|
}
|
||||||
|
|
||||||
function initPortfolioEditor(options) {
|
function initPortfolioEditor(options) {
|
||||||
const form = document.querySelector(options.formSelector);
|
const form = document.querySelector(options.formSelector);
|
||||||
const editorElement = document.querySelector(options.editorSelector);
|
const editorElement = document.querySelector(options.editorSelector);
|
||||||
@@ -501,34 +663,22 @@ function initPortfolioEditor(options) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
attachToolbarActions(editor, toolbar, fileInput);
|
attachToolbarActions(editor, toolbar, fileInput);
|
||||||
|
attachImageDropUpload(editor, editorElement, options, form);
|
||||||
|
|
||||||
fileInput.addEventListener("change", async () => {
|
fileInput.addEventListener("change", async () => {
|
||||||
if (!fileInput.files?.length) {
|
if (!fileInput.files?.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [file] = fileInput.files;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = await uploadImage(file, options, form);
|
await uploadAndInsertImages(
|
||||||
const shouldBeRepresentative = !getRepresentativeImage(editor);
|
fileInput.files,
|
||||||
|
editor,
|
||||||
editor
|
options,
|
||||||
.chain()
|
form,
|
||||||
.focus()
|
null,
|
||||||
.insertContent({
|
editorElement,
|
||||||
type: PORTFOLIO_IMAGE_TYPE,
|
);
|
||||||
attrs: {
|
|
||||||
src: payload.url,
|
|
||||||
alt: payload.orgnlFileNm || file.name,
|
|
||||||
atchFileIdntfNo: payload.atchFileIdntfNo,
|
|
||||||
fileSn: payload.fileSn,
|
|
||||||
isRepresentative: shouldBeRepresentative,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.run();
|
|
||||||
} catch (error) {
|
|
||||||
window.alert(error.message || "이미지 업로드에 실패했습니다.");
|
|
||||||
} finally {
|
} finally {
|
||||||
fileInput.value = "";
|
fileInput.value = "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,325 @@
|
|||||||
|
import {
|
||||||
|
AllCommunityModule,
|
||||||
|
ModuleRegistry,
|
||||||
|
createGrid,
|
||||||
|
} from "ag-grid-community";
|
||||||
|
import "ag-grid-community/styles/ag-grid.css";
|
||||||
|
import "ag-grid-community/styles/ag-theme-quartz.css";
|
||||||
|
|
||||||
|
ModuleRegistry.registerModules([AllCommunityModule]);
|
||||||
|
|
||||||
|
const DEFAULT_SELECTOR = "[data-portfolio-list-panel]";
|
||||||
|
const DEFAULT_PAGE_SIZE = 5;
|
||||||
|
|
||||||
|
function toDate(value) {
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(value);
|
||||||
|
return Number.isNaN(date.getTime()) ? null : date;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(value) {
|
||||||
|
const date = toDate(value);
|
||||||
|
if (!date) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat("ko-KR", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
}).format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRelativeDate(value) {
|
||||||
|
const date = toDate(value);
|
||||||
|
if (!date) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const diffMs = Date.now() - date.getTime();
|
||||||
|
if (diffMs < 0) {
|
||||||
|
return formatDate(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const diffMinutes = Math.floor(diffMs / 60000);
|
||||||
|
if (diffMinutes < 1) {
|
||||||
|
return "방금 전";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diffMinutes < 60) {
|
||||||
|
return `${diffMinutes}분 전`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const diffHours = Math.floor(diffMinutes / 60);
|
||||||
|
if (diffHours < 24) {
|
||||||
|
return `${diffHours}시간 전`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatDate(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPostUrl(portfolioIdntfNo) {
|
||||||
|
return `/portfolio/${encodeURIComponent(portfolioIdntfNo)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTitleCell(params) {
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.className = "portfolio-post-title-link";
|
||||||
|
link.href = getPostUrl(params.data.portfolioIdntfNo);
|
||||||
|
link.textContent = params.value || "";
|
||||||
|
link.title = params.value || "";
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPageButton(label, options = {}) {
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.className = "portfolio-post-page-button";
|
||||||
|
button.textContent = label;
|
||||||
|
|
||||||
|
if (options.active) {
|
||||||
|
button.classList.add("is-active");
|
||||||
|
button.setAttribute("aria-current", "page");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.disabled) {
|
||||||
|
button.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof options.onClick === "function") {
|
||||||
|
button.addEventListener("click", options.onClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPagination(gridApi, paginationElement) {
|
||||||
|
if (!gridApi || !paginationElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalPages = gridApi.paginationGetTotalPages();
|
||||||
|
const currentPage = gridApi.paginationGetCurrentPage();
|
||||||
|
paginationElement.replaceChildren();
|
||||||
|
|
||||||
|
if (totalPages <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxVisiblePages = 10;
|
||||||
|
const currentPageGroup = Math.floor(currentPage / maxVisiblePages);
|
||||||
|
const startPage = currentPageGroup * maxVisiblePages;
|
||||||
|
const endPage = Math.min(startPage + maxVisiblePages, totalPages);
|
||||||
|
|
||||||
|
if (startPage > 0) {
|
||||||
|
paginationElement.appendChild(
|
||||||
|
createPageButton("이전", {
|
||||||
|
onClick: () => gridApi.paginationGoToPage(startPage - 1),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let page = startPage; page < endPage; page += 1) {
|
||||||
|
paginationElement.appendChild(
|
||||||
|
createPageButton(String(page + 1), {
|
||||||
|
active: page === currentPage,
|
||||||
|
onClick: () => gridApi.paginationGoToPage(page),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endPage < totalPages) {
|
||||||
|
paginationElement.appendChild(
|
||||||
|
createPageButton("다음 >", {
|
||||||
|
onClick: () => gridApi.paginationGoToPage(endPage),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGridOptions(rows, currentId, paginationElement) {
|
||||||
|
return {
|
||||||
|
rowData: rows,
|
||||||
|
columnDefs: [
|
||||||
|
{
|
||||||
|
field: "title",
|
||||||
|
headerName: "글 제목",
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 260,
|
||||||
|
cellRenderer: createTitleCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "viewCount",
|
||||||
|
headerName: "조회수",
|
||||||
|
width: 96,
|
||||||
|
type: "numericColumn",
|
||||||
|
headerClass: "portfolio-post-number-header",
|
||||||
|
cellClass: "portfolio-post-number-cell",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "registeredAt",
|
||||||
|
headerName: "작성일",
|
||||||
|
width: 132,
|
||||||
|
valueFormatter: (params) => formatRelativeDate(params.value),
|
||||||
|
headerClass: "portfolio-post-date-header",
|
||||||
|
cellClass: "portfolio-post-date-cell",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defaultColDef: {
|
||||||
|
sortable: true,
|
||||||
|
resizable: false,
|
||||||
|
suppressMovable: true,
|
||||||
|
},
|
||||||
|
domLayout: "autoHeight",
|
||||||
|
headerHeight: 34,
|
||||||
|
rowHeight: 40,
|
||||||
|
pagination: true,
|
||||||
|
paginationPageSize: DEFAULT_PAGE_SIZE,
|
||||||
|
suppressPaginationPanel: true,
|
||||||
|
suppressCellFocus: true,
|
||||||
|
suppressDragLeaveHidesColumns: true,
|
||||||
|
suppressHorizontalScroll: true,
|
||||||
|
getRowId: (params) => params.data.portfolioIdntfNo,
|
||||||
|
getRowClass: (params) =>
|
||||||
|
params.data.portfolioIdntfNo === currentId ? "ag-row-current-post" : "",
|
||||||
|
onGridReady: (params) => renderPagination(params.api, paginationElement),
|
||||||
|
onPaginationChanged: (event) => renderPagination(event.api, paginationElement),
|
||||||
|
localeText: {
|
||||||
|
page: "페이지",
|
||||||
|
more: "더 보기",
|
||||||
|
to: "-",
|
||||||
|
of: "/",
|
||||||
|
next: "다음",
|
||||||
|
last: "마지막",
|
||||||
|
first: "처음",
|
||||||
|
previous: "이전",
|
||||||
|
loadingOoo: "불러오는 중...",
|
||||||
|
noRowsToShow: "등록된 글이 없습니다.",
|
||||||
|
pageSizeSelectorLabel: "줄 보기",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchPortfolioItems(listUrl) {
|
||||||
|
const response = await fetch(listUrl, {
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
credentials: "same-origin",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("포트폴리오 목록을 불러오지 못했습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await response.json();
|
||||||
|
return Array.isArray(rows) ? rows : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function initPortfolioListPanel(selector = DEFAULT_SELECTOR) {
|
||||||
|
const panel = document.querySelector(selector);
|
||||||
|
if (!panel) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleButton = panel.querySelector("[data-portfolio-list-toggle]");
|
||||||
|
const body = panel.querySelector("[data-portfolio-list-body]");
|
||||||
|
const gridElement = panel.querySelector("[data-portfolio-list-grid]");
|
||||||
|
const paginationElement = panel.querySelector("[data-portfolio-pagination]");
|
||||||
|
const pageSizeSelect = panel.querySelector("[data-portfolio-page-size]");
|
||||||
|
const countElement = panel.querySelector(".portfolio-post-list-summary span");
|
||||||
|
const listUrl = panel.dataset.listUrl;
|
||||||
|
const currentId = panel.dataset.currentId || "";
|
||||||
|
const initialOpen = panel.dataset.initialOpen === "true";
|
||||||
|
|
||||||
|
if (!toggleButton || !body || !gridElement || !paginationElement || !listUrl) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isOpen = false;
|
||||||
|
let rowsPromise = null;
|
||||||
|
let gridApi = null;
|
||||||
|
|
||||||
|
const loadRows = () => {
|
||||||
|
rowsPromise ||= fetchPortfolioItems(listUrl);
|
||||||
|
return rowsPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderRows = async () => {
|
||||||
|
const rows = await loadRows();
|
||||||
|
|
||||||
|
if (countElement) {
|
||||||
|
countElement.textContent = `${rows.length}개의 글`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!gridApi) {
|
||||||
|
gridApi = createGrid(
|
||||||
|
gridElement,
|
||||||
|
createGridOptions(rows, currentId, paginationElement),
|
||||||
|
);
|
||||||
|
renderPagination(gridApi, paginationElement);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gridApi.setGridOption("rowData", rows);
|
||||||
|
renderPagination(gridApi, paginationElement);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setOpen = async (nextOpen) => {
|
||||||
|
isOpen = nextOpen;
|
||||||
|
toggleButton.textContent = isOpen ? "목록닫기" : "목록열기";
|
||||||
|
toggleButton.setAttribute("aria-expanded", String(isOpen));
|
||||||
|
body.hidden = !isOpen;
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleButton.disabled = true;
|
||||||
|
try {
|
||||||
|
await renderRows();
|
||||||
|
} catch (error) {
|
||||||
|
window.alert(error.message || "포트폴리오 목록을 불러오지 못했습니다.");
|
||||||
|
setOpen(false);
|
||||||
|
} finally {
|
||||||
|
toggleButton.disabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleButton.addEventListener("click", () => {
|
||||||
|
void setOpen(!isOpen);
|
||||||
|
});
|
||||||
|
|
||||||
|
pageSizeSelect?.addEventListener("change", () => {
|
||||||
|
if (!gridApi) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gridApi.setGridOption(
|
||||||
|
"paginationPageSize",
|
||||||
|
Number.parseInt(pageSizeSelect.value, 10) || DEFAULT_PAGE_SIZE,
|
||||||
|
);
|
||||||
|
gridApi.paginationGoToPage(0);
|
||||||
|
renderPagination(gridApi, paginationElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
void setOpen(initialOpen);
|
||||||
|
|
||||||
|
return {
|
||||||
|
open: () => setOpen(true),
|
||||||
|
close: () => setOpen(false),
|
||||||
|
reload: async () => {
|
||||||
|
rowsPromise = null;
|
||||||
|
await renderRows();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
initPortfolioListPanel();
|
||||||
|
});
|
||||||
|
|
||||||
|
export { initPortfolioListPanel };
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package kr.iotdoor.boot.mapper;
|
||||||
|
|
||||||
|
import kr.iotdoor.boot.model.statistics.AccessDailyStat;
|
||||||
|
import kr.iotdoor.boot.model.statistics.AccessLog;
|
||||||
|
import kr.iotdoor.boot.model.statistics.AccessStatItem;
|
||||||
|
import org.apache.ibatis.annotations.Insert;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface AccessStatisticsMapper {
|
||||||
|
|
||||||
|
@Insert("""
|
||||||
|
INSERT INTO TN_ACCESS_LOG (
|
||||||
|
ACCESS_LOG_IDNTF_NO,
|
||||||
|
SESSION_ID,
|
||||||
|
REFERER_URL,
|
||||||
|
ACCESS_IP,
|
||||||
|
HTTP_METHOD,
|
||||||
|
REQUEST_URI,
|
||||||
|
QUERY_STRING,
|
||||||
|
USER_AGENT,
|
||||||
|
STATUS_CD,
|
||||||
|
REG_DT
|
||||||
|
) VALUES (
|
||||||
|
#{accessLogIdntfNo},
|
||||||
|
#{sessionId},
|
||||||
|
#{refererUrl},
|
||||||
|
#{accessIp},
|
||||||
|
#{httpMethod},
|
||||||
|
#{requestUri},
|
||||||
|
#{queryString},
|
||||||
|
#{userAgent},
|
||||||
|
#{statusCode},
|
||||||
|
CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
int insertAccessLog(AccessLog accessLog);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM TN_ACCESS_LOG
|
||||||
|
""")
|
||||||
|
long countAll();
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM TN_ACCESS_LOG
|
||||||
|
WHERE REG_DT >= #{startAt}
|
||||||
|
AND REG_DT < #{endAt}
|
||||||
|
""")
|
||||||
|
long countBetween(
|
||||||
|
@Param("startAt") LocalDateTime startAt,
|
||||||
|
@Param("endAt") LocalDateTime endAt
|
||||||
|
);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT COUNT(DISTINCT ACCESS_IP)
|
||||||
|
FROM TN_ACCESS_LOG
|
||||||
|
WHERE ACCESS_IP IS NOT NULL
|
||||||
|
AND ACCESS_IP <> ''
|
||||||
|
""")
|
||||||
|
long countUniqueIp();
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT COUNT(DISTINCT SESSION_ID)
|
||||||
|
FROM TN_ACCESS_LOG
|
||||||
|
WHERE SESSION_ID IS NOT NULL
|
||||||
|
AND SESSION_ID <> ''
|
||||||
|
""")
|
||||||
|
long countUniqueSession();
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT
|
||||||
|
ACCESS_LOG_IDNTF_NO,
|
||||||
|
SESSION_ID,
|
||||||
|
REFERER_URL,
|
||||||
|
ACCESS_IP,
|
||||||
|
HTTP_METHOD,
|
||||||
|
REQUEST_URI,
|
||||||
|
QUERY_STRING,
|
||||||
|
USER_AGENT,
|
||||||
|
STATUS_CD AS STATUS_CODE,
|
||||||
|
REG_DT AS REGISTERED_AT
|
||||||
|
FROM TN_ACCESS_LOG
|
||||||
|
ORDER BY REG_DT DESC
|
||||||
|
LIMIT #{limit}
|
||||||
|
""")
|
||||||
|
List<AccessLog> selectRecentLogs(@Param("limit") int limit);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT
|
||||||
|
CAST(REG_DT AS DATE) AS ACCESS_DATE,
|
||||||
|
COUNT(*) AS ACCESS_COUNT
|
||||||
|
FROM TN_ACCESS_LOG
|
||||||
|
WHERE REG_DT >= #{startAt}
|
||||||
|
GROUP BY CAST(REG_DT AS DATE)
|
||||||
|
ORDER BY ACCESS_DATE DESC
|
||||||
|
LIMIT #{limit}
|
||||||
|
""")
|
||||||
|
List<AccessDailyStat> selectDailyStats(
|
||||||
|
@Param("startAt") LocalDateTime startAt,
|
||||||
|
@Param("limit") int limit
|
||||||
|
);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT
|
||||||
|
REQUEST_URI AS LABEL,
|
||||||
|
COUNT(*) AS ACCESS_COUNT
|
||||||
|
FROM TN_ACCESS_LOG
|
||||||
|
GROUP BY REQUEST_URI
|
||||||
|
ORDER BY ACCESS_COUNT DESC, LABEL ASC
|
||||||
|
LIMIT #{limit}
|
||||||
|
""")
|
||||||
|
List<AccessStatItem> selectTopPages(@Param("limit") int limit);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT
|
||||||
|
CASE
|
||||||
|
WHEN REFERER_URL IS NULL OR REFERER_URL = '' THEN '직접 접속'
|
||||||
|
ELSE REFERER_URL
|
||||||
|
END AS LABEL,
|
||||||
|
COUNT(*) AS ACCESS_COUNT
|
||||||
|
FROM TN_ACCESS_LOG
|
||||||
|
GROUP BY
|
||||||
|
CASE
|
||||||
|
WHEN REFERER_URL IS NULL OR REFERER_URL = '' THEN '직접 접속'
|
||||||
|
ELSE REFERER_URL
|
||||||
|
END
|
||||||
|
ORDER BY ACCESS_COUNT DESC, LABEL ASC
|
||||||
|
LIMIT #{limit}
|
||||||
|
""")
|
||||||
|
List<AccessStatItem> selectTopReferers(@Param("limit") int limit);
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
package kr.iotdoor.boot.mapper;
|
||||||
|
|
||||||
|
import kr.iotdoor.boot.model.banner.MainBanner;
|
||||||
|
import org.apache.ibatis.annotations.Insert;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
import org.apache.ibatis.annotations.Update;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface MainBannerMapper {
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT
|
||||||
|
BANNER_IDNTF_NO,
|
||||||
|
TTL AS TITLE,
|
||||||
|
CN AS CONTENT,
|
||||||
|
IMAGE_ATCH_FILE_IDNTF_NO,
|
||||||
|
IMAGE_FILE_SN,
|
||||||
|
IMAGE_URL,
|
||||||
|
SORT_ORDR AS SORT_ORDER,
|
||||||
|
USE_YN,
|
||||||
|
DEL_YN,
|
||||||
|
REG_DT AS REGISTERED_AT,
|
||||||
|
UPD_DT AS UPDATED_AT
|
||||||
|
FROM TN_MAIN_BANNER
|
||||||
|
WHERE DEL_YN = 'N'
|
||||||
|
ORDER BY SORT_ORDR ASC, REG_DT ASC
|
||||||
|
""")
|
||||||
|
List<MainBanner> selectAdminBanners();
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT
|
||||||
|
BANNER_IDNTF_NO,
|
||||||
|
TTL AS TITLE,
|
||||||
|
CN AS CONTENT,
|
||||||
|
IMAGE_ATCH_FILE_IDNTF_NO,
|
||||||
|
IMAGE_FILE_SN,
|
||||||
|
IMAGE_URL,
|
||||||
|
SORT_ORDR AS SORT_ORDER,
|
||||||
|
USE_YN,
|
||||||
|
DEL_YN,
|
||||||
|
REG_DT AS REGISTERED_AT,
|
||||||
|
UPD_DT AS UPDATED_AT
|
||||||
|
FROM TN_MAIN_BANNER
|
||||||
|
WHERE DEL_YN = 'N'
|
||||||
|
AND USE_YN = 'Y'
|
||||||
|
ORDER BY SORT_ORDR ASC, REG_DT ASC
|
||||||
|
""")
|
||||||
|
List<MainBanner> selectPublicBanners();
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT
|
||||||
|
BANNER_IDNTF_NO,
|
||||||
|
TTL AS TITLE,
|
||||||
|
CN AS CONTENT,
|
||||||
|
IMAGE_ATCH_FILE_IDNTF_NO,
|
||||||
|
IMAGE_FILE_SN,
|
||||||
|
IMAGE_URL,
|
||||||
|
SORT_ORDR AS SORT_ORDER,
|
||||||
|
USE_YN,
|
||||||
|
DEL_YN,
|
||||||
|
REG_DT AS REGISTERED_AT,
|
||||||
|
UPD_DT AS UPDATED_AT
|
||||||
|
FROM TN_MAIN_BANNER
|
||||||
|
WHERE BANNER_IDNTF_NO = #{bannerIdntfNo}
|
||||||
|
AND DEL_YN = 'N'
|
||||||
|
""")
|
||||||
|
MainBanner selectBanner(@Param("bannerIdntfNo") String bannerIdntfNo);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT
|
||||||
|
BANNER_IDNTF_NO,
|
||||||
|
TTL AS TITLE,
|
||||||
|
CN AS CONTENT,
|
||||||
|
IMAGE_ATCH_FILE_IDNTF_NO,
|
||||||
|
IMAGE_FILE_SN,
|
||||||
|
IMAGE_URL,
|
||||||
|
SORT_ORDR AS SORT_ORDER,
|
||||||
|
USE_YN,
|
||||||
|
DEL_YN,
|
||||||
|
REG_DT AS REGISTERED_AT,
|
||||||
|
UPD_DT AS UPDATED_AT
|
||||||
|
FROM TN_MAIN_BANNER
|
||||||
|
WHERE DEL_YN = 'N'
|
||||||
|
AND SORT_ORDR < #{sortOrder}
|
||||||
|
ORDER BY SORT_ORDR DESC, REG_DT DESC
|
||||||
|
LIMIT 1
|
||||||
|
""")
|
||||||
|
MainBanner selectPreviousBanner(@Param("sortOrder") int sortOrder);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT
|
||||||
|
BANNER_IDNTF_NO,
|
||||||
|
TTL AS TITLE,
|
||||||
|
CN AS CONTENT,
|
||||||
|
IMAGE_ATCH_FILE_IDNTF_NO,
|
||||||
|
IMAGE_FILE_SN,
|
||||||
|
IMAGE_URL,
|
||||||
|
SORT_ORDR AS SORT_ORDER,
|
||||||
|
USE_YN,
|
||||||
|
DEL_YN,
|
||||||
|
REG_DT AS REGISTERED_AT,
|
||||||
|
UPD_DT AS UPDATED_AT
|
||||||
|
FROM TN_MAIN_BANNER
|
||||||
|
WHERE DEL_YN = 'N'
|
||||||
|
AND SORT_ORDR > #{sortOrder}
|
||||||
|
ORDER BY SORT_ORDR ASC, REG_DT ASC
|
||||||
|
LIMIT 1
|
||||||
|
""")
|
||||||
|
MainBanner selectNextBanner(@Param("sortOrder") int sortOrder);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT COALESCE(MAX(SORT_ORDR), 0)
|
||||||
|
FROM TN_MAIN_BANNER
|
||||||
|
WHERE DEL_YN = 'N'
|
||||||
|
""")
|
||||||
|
int selectMaxSortOrder();
|
||||||
|
|
||||||
|
@Insert("""
|
||||||
|
INSERT INTO TN_MAIN_BANNER (
|
||||||
|
BANNER_IDNTF_NO,
|
||||||
|
TTL,
|
||||||
|
CN,
|
||||||
|
IMAGE_ATCH_FILE_IDNTF_NO,
|
||||||
|
IMAGE_FILE_SN,
|
||||||
|
IMAGE_URL,
|
||||||
|
SORT_ORDR,
|
||||||
|
USE_YN,
|
||||||
|
DEL_YN,
|
||||||
|
REG_DT,
|
||||||
|
UPD_DT
|
||||||
|
) VALUES (
|
||||||
|
#{bannerIdntfNo},
|
||||||
|
#{title},
|
||||||
|
#{content},
|
||||||
|
#{imageAtchFileIdntfNo},
|
||||||
|
#{imageFileSn},
|
||||||
|
#{imageUrl},
|
||||||
|
#{sortOrder},
|
||||||
|
#{useYn},
|
||||||
|
'N',
|
||||||
|
CURRENT_TIMESTAMP,
|
||||||
|
NULL
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
int insertBanner(MainBanner mainBanner);
|
||||||
|
|
||||||
|
@Update("""
|
||||||
|
UPDATE TN_MAIN_BANNER
|
||||||
|
SET
|
||||||
|
TTL = #{title},
|
||||||
|
CN = #{content},
|
||||||
|
IMAGE_ATCH_FILE_IDNTF_NO = #{imageAtchFileIdntfNo},
|
||||||
|
IMAGE_FILE_SN = #{imageFileSn},
|
||||||
|
IMAGE_URL = #{imageUrl},
|
||||||
|
USE_YN = #{useYn},
|
||||||
|
UPD_DT = CURRENT_TIMESTAMP
|
||||||
|
WHERE BANNER_IDNTF_NO = #{bannerIdntfNo}
|
||||||
|
AND DEL_YN = 'N'
|
||||||
|
""")
|
||||||
|
int updateBanner(MainBanner mainBanner);
|
||||||
|
|
||||||
|
@Update("""
|
||||||
|
UPDATE TN_MAIN_BANNER
|
||||||
|
SET
|
||||||
|
SORT_ORDR = #{sortOrder},
|
||||||
|
UPD_DT = CURRENT_TIMESTAMP
|
||||||
|
WHERE BANNER_IDNTF_NO = #{bannerIdntfNo}
|
||||||
|
AND DEL_YN = 'N'
|
||||||
|
""")
|
||||||
|
int updateSortOrder(
|
||||||
|
@Param("bannerIdntfNo") String bannerIdntfNo,
|
||||||
|
@Param("sortOrder") int sortOrder
|
||||||
|
);
|
||||||
|
|
||||||
|
@Update("""
|
||||||
|
UPDATE TN_MAIN_BANNER
|
||||||
|
SET
|
||||||
|
DEL_YN = 'Y',
|
||||||
|
UPD_DT = CURRENT_TIMESTAMP
|
||||||
|
WHERE BANNER_IDNTF_NO = #{bannerIdntfNo}
|
||||||
|
""")
|
||||||
|
int deleteBanner(@Param("bannerIdntfNo") String bannerIdntfNo);
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package kr.iotdoor.boot.model.banner;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public class MainBanner {
|
||||||
|
|
||||||
|
private String bannerIdntfNo;
|
||||||
|
private String title;
|
||||||
|
private String content;
|
||||||
|
private String imageAtchFileIdntfNo;
|
||||||
|
private Integer imageFileSn;
|
||||||
|
private String imageUrl;
|
||||||
|
private int sortOrder;
|
||||||
|
private String useYn;
|
||||||
|
private String delYn;
|
||||||
|
private LocalDateTime registeredAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
public String getBannerIdntfNo() {
|
||||||
|
return bannerIdntfNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBannerIdntfNo(String bannerIdntfNo) {
|
||||||
|
this.bannerIdntfNo = bannerIdntfNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContent() {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContent(String content) {
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getImageAtchFileIdntfNo() {
|
||||||
|
return imageAtchFileIdntfNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImageAtchFileIdntfNo(String imageAtchFileIdntfNo) {
|
||||||
|
this.imageAtchFileIdntfNo = imageAtchFileIdntfNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getImageFileSn() {
|
||||||
|
return imageFileSn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImageFileSn(Integer imageFileSn) {
|
||||||
|
this.imageFileSn = imageFileSn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getImageUrl() {
|
||||||
|
return imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImageUrl(String imageUrl) {
|
||||||
|
this.imageUrl = imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSortOrder() {
|
||||||
|
return sortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSortOrder(int sortOrder) {
|
||||||
|
this.sortOrder = sortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUseYn() {
|
||||||
|
return useYn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUseYn(String useYn) {
|
||||||
|
this.useYn = useYn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDelYn() {
|
||||||
|
return delYn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDelYn(String delYn) {
|
||||||
|
this.delYn = delYn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getRegisteredAt() {
|
||||||
|
return registeredAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRegisteredAt(LocalDateTime registeredAt) {
|
||||||
|
this.registeredAt = registeredAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getUpdatedAt() {
|
||||||
|
return updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||||
|
this.updatedAt = updatedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package kr.iotdoor.boot.model.banner;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
|
||||||
|
public class MainBannerForm {
|
||||||
|
|
||||||
|
private String bannerIdntfNo;
|
||||||
|
|
||||||
|
@NotBlank(message = "제목을 입력해주세요.")
|
||||||
|
@Size(max = 200, message = "제목은 200자 이하로 입력해주세요.")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@NotBlank(message = "내용을 입력해주세요.")
|
||||||
|
@Size(max = 1000, message = "내용은 1000자 이하로 입력해주세요.")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
private String imageAtchFileIdntfNo;
|
||||||
|
private Integer imageFileSn;
|
||||||
|
private String imageUrl;
|
||||||
|
private String useYn = "Y";
|
||||||
|
|
||||||
|
public String getBannerIdntfNo() {
|
||||||
|
return bannerIdntfNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBannerIdntfNo(String bannerIdntfNo) {
|
||||||
|
this.bannerIdntfNo = bannerIdntfNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContent() {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContent(String content) {
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getImageAtchFileIdntfNo() {
|
||||||
|
return imageAtchFileIdntfNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImageAtchFileIdntfNo(String imageAtchFileIdntfNo) {
|
||||||
|
this.imageAtchFileIdntfNo = imageAtchFileIdntfNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getImageFileSn() {
|
||||||
|
return imageFileSn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImageFileSn(Integer imageFileSn) {
|
||||||
|
this.imageFileSn = imageFileSn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getImageUrl() {
|
||||||
|
return imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImageUrl(String imageUrl) {
|
||||||
|
this.imageUrl = imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUseYn() {
|
||||||
|
return useYn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUseYn(String useYn) {
|
||||||
|
this.useYn = useYn;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
package kr.iotdoor.boot.model.portfolio;
|
package kr.iotdoor.boot.model.portfolio;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
public class PortfolioForm {
|
public class PortfolioForm {
|
||||||
|
|
||||||
@@ -15,6 +18,9 @@ public class PortfolioForm {
|
|||||||
private String thumbnailAtchFileIdntfNo;
|
private String thumbnailAtchFileIdntfNo;
|
||||||
private int thumbnailFileSn;
|
private int thumbnailFileSn;
|
||||||
|
|
||||||
|
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
||||||
|
private LocalDateTime registeredAt;
|
||||||
|
|
||||||
public String getPortfolioIdntfNo() {
|
public String getPortfolioIdntfNo() {
|
||||||
return portfolioIdntfNo;
|
return portfolioIdntfNo;
|
||||||
}
|
}
|
||||||
@@ -54,4 +60,12 @@ public class PortfolioForm {
|
|||||||
public void setThumbnailFileSn(int thumbnailFileSn) {
|
public void setThumbnailFileSn(int thumbnailFileSn) {
|
||||||
this.thumbnailFileSn = thumbnailFileSn;
|
this.thumbnailFileSn = thumbnailFileSn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getRegisteredAt() {
|
||||||
|
return registeredAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRegisteredAt(LocalDateTime registeredAt) {
|
||||||
|
this.registeredAt = registeredAt;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package kr.iotdoor.boot.model.statistics;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
public class AccessDailyStat {
|
||||||
|
|
||||||
|
private LocalDate accessDate;
|
||||||
|
private long accessCount;
|
||||||
|
|
||||||
|
public LocalDate getAccessDate() {
|
||||||
|
return accessDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAccessDate(LocalDate accessDate) {
|
||||||
|
this.accessDate = accessDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getAccessCount() {
|
||||||
|
return accessCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAccessCount(long accessCount) {
|
||||||
|
this.accessCount = accessCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package kr.iotdoor.boot.model.statistics;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public class AccessLog {
|
||||||
|
|
||||||
|
private String accessLogIdntfNo;
|
||||||
|
private String sessionId;
|
||||||
|
private String refererUrl;
|
||||||
|
private String accessIp;
|
||||||
|
private String httpMethod;
|
||||||
|
private String requestUri;
|
||||||
|
private String queryString;
|
||||||
|
private String userAgent;
|
||||||
|
private Integer statusCode;
|
||||||
|
private LocalDateTime registeredAt;
|
||||||
|
|
||||||
|
public String getAccessLogIdntfNo() {
|
||||||
|
return accessLogIdntfNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAccessLogIdntfNo(String accessLogIdntfNo) {
|
||||||
|
this.accessLogIdntfNo = accessLogIdntfNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSessionId() {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionId(String sessionId) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRefererUrl() {
|
||||||
|
return refererUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRefererUrl(String refererUrl) {
|
||||||
|
this.refererUrl = refererUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAccessIp() {
|
||||||
|
return accessIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAccessIp(String accessIp) {
|
||||||
|
this.accessIp = accessIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHttpMethod() {
|
||||||
|
return httpMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHttpMethod(String httpMethod) {
|
||||||
|
this.httpMethod = httpMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRequestUri() {
|
||||||
|
return requestUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequestUri(String requestUri) {
|
||||||
|
this.requestUri = requestUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getQueryString() {
|
||||||
|
return queryString;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setQueryString(String queryString) {
|
||||||
|
this.queryString = queryString;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserAgent() {
|
||||||
|
return userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserAgent(String userAgent) {
|
||||||
|
this.userAgent = userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getStatusCode() {
|
||||||
|
return statusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatusCode(Integer statusCode) {
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getRegisteredAt() {
|
||||||
|
return registeredAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRegisteredAt(LocalDateTime registeredAt) {
|
||||||
|
this.registeredAt = registeredAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFullRequestPath() {
|
||||||
|
if (queryString == null || queryString.isBlank()) {
|
||||||
|
return requestUri;
|
||||||
|
}
|
||||||
|
return requestUri + "?" + queryString;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package kr.iotdoor.boot.model.statistics;
|
||||||
|
|
||||||
|
public class AccessStatItem {
|
||||||
|
|
||||||
|
private String label;
|
||||||
|
private long accessCount;
|
||||||
|
|
||||||
|
public String getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLabel(String label) {
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getAccessCount() {
|
||||||
|
return accessCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAccessCount(long accessCount) {
|
||||||
|
this.accessCount = accessCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package kr.iotdoor.boot.model.statistics;
|
||||||
|
|
||||||
|
public class AccessStatisticsSummary {
|
||||||
|
|
||||||
|
private long totalAccessCount;
|
||||||
|
private long todayAccessCount;
|
||||||
|
private long uniqueIpCount;
|
||||||
|
private long uniqueSessionCount;
|
||||||
|
|
||||||
|
public long getTotalAccessCount() {
|
||||||
|
return totalAccessCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotalAccessCount(long totalAccessCount) {
|
||||||
|
this.totalAccessCount = totalAccessCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTodayAccessCount() {
|
||||||
|
return todayAccessCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTodayAccessCount(long todayAccessCount) {
|
||||||
|
this.todayAccessCount = todayAccessCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getUniqueIpCount() {
|
||||||
|
return uniqueIpCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUniqueIpCount(long uniqueIpCount) {
|
||||||
|
this.uniqueIpCount = uniqueIpCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getUniqueSessionCount() {
|
||||||
|
return uniqueSessionCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUniqueSessionCount(long uniqueSessionCount) {
|
||||||
|
this.uniqueSessionCount = uniqueSessionCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package kr.iotdoor.boot.service;
|
||||||
|
|
||||||
|
import kr.iotdoor.boot.mapper.AccessStatisticsMapper;
|
||||||
|
import kr.iotdoor.boot.model.statistics.AccessDailyStat;
|
||||||
|
import kr.iotdoor.boot.model.statistics.AccessLog;
|
||||||
|
import kr.iotdoor.boot.model.statistics.AccessStatItem;
|
||||||
|
import kr.iotdoor.boot.model.statistics.AccessStatisticsSummary;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AccessStatisticsService {
|
||||||
|
|
||||||
|
private static final ZoneId DEFAULT_ZONE = ZoneId.of("Asia/Seoul");
|
||||||
|
|
||||||
|
private final AccessStatisticsMapper accessStatisticsMapper;
|
||||||
|
|
||||||
|
public AccessStatisticsService(AccessStatisticsMapper accessStatisticsMapper) {
|
||||||
|
this.accessStatisticsMapper = accessStatisticsMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void record(AccessLog accessLog) {
|
||||||
|
accessLog.setAccessLogIdntfNo(newIdentifier());
|
||||||
|
accessStatisticsMapper.insertAccessLog(accessLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public AccessStatisticsSummary getSummary() {
|
||||||
|
LocalDate today = LocalDate.now(DEFAULT_ZONE);
|
||||||
|
AccessStatisticsSummary summary = new AccessStatisticsSummary();
|
||||||
|
summary.setTotalAccessCount(accessStatisticsMapper.countAll());
|
||||||
|
summary.setTodayAccessCount(accessStatisticsMapper.countBetween(today.atStartOfDay(), today.plusDays(1).atStartOfDay()));
|
||||||
|
summary.setUniqueIpCount(accessStatisticsMapper.countUniqueIp());
|
||||||
|
summary.setUniqueSessionCount(accessStatisticsMapper.countUniqueSession());
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<AccessLog> getRecentLogs(int limit) {
|
||||||
|
return accessStatisticsMapper.selectRecentLogs(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<AccessDailyStat> getDailyStats(int days) {
|
||||||
|
LocalDate startDate = LocalDate.now(DEFAULT_ZONE).minusDays(Math.max(days - 1, 0));
|
||||||
|
return accessStatisticsMapper.selectDailyStats(startDate.atStartOfDay(), days);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<AccessStatItem> getTopPages(int limit) {
|
||||||
|
return accessStatisticsMapper.selectTopPages(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<AccessStatItem> getTopReferers(int limit) {
|
||||||
|
return accessStatisticsMapper.selectTopReferers(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String newIdentifier() {
|
||||||
|
return UUID.randomUUID().toString().replace("-", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
package kr.iotdoor.boot.service;
|
||||||
|
|
||||||
|
import kr.iotdoor.boot.mapper.MainBannerMapper;
|
||||||
|
import kr.iotdoor.boot.model.banner.MainBanner;
|
||||||
|
import kr.iotdoor.boot.model.banner.MainBannerForm;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class MainBannerService {
|
||||||
|
|
||||||
|
private static final String FALLBACK_IMAGE = "/resources/kr/iotdoor/comn/site/layout/image/common/no_img.svg";
|
||||||
|
|
||||||
|
private final MainBannerMapper mainBannerMapper;
|
||||||
|
private final FileStorageService fileStorageService;
|
||||||
|
|
||||||
|
public MainBannerService(MainBannerMapper mainBannerMapper, FileStorageService fileStorageService) {
|
||||||
|
this.mainBannerMapper = mainBannerMapper;
|
||||||
|
this.fileStorageService = fileStorageService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<MainBanner> getPublicBanners() {
|
||||||
|
return mainBannerMapper.selectPublicBanners().stream()
|
||||||
|
.map(this::enrich)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<MainBanner> getAdminBanners() {
|
||||||
|
return mainBannerMapper.selectAdminBanners().stream()
|
||||||
|
.map(this::enrich)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public MainBannerForm getForm(String bannerIdntfNo) {
|
||||||
|
MainBannerForm form = new MainBannerForm();
|
||||||
|
form.setUseYn("Y");
|
||||||
|
|
||||||
|
if (!StringUtils.hasText(bannerIdntfNo)) {
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
|
||||||
|
MainBanner banner = mainBannerMapper.selectBanner(bannerIdntfNo);
|
||||||
|
if (banner == null) {
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.setBannerIdntfNo(banner.getBannerIdntfNo());
|
||||||
|
form.setTitle(banner.getTitle());
|
||||||
|
form.setContent(banner.getContent());
|
||||||
|
form.setImageAtchFileIdntfNo(banner.getImageAtchFileIdntfNo());
|
||||||
|
form.setImageFileSn(banner.getImageFileSn());
|
||||||
|
form.setImageUrl(banner.getImageUrl());
|
||||||
|
form.setUseYn("Y".equals(banner.getUseYn()) ? "Y" : "N");
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public String save(MainBannerForm form) {
|
||||||
|
boolean hasUploadedImage = hasUploadedImage(form);
|
||||||
|
if (!hasUploadedImage && !StringUtils.hasText(form.getImageUrl())) {
|
||||||
|
throw new IllegalArgumentException("배너 이미지는 필수입니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.hasText(form.getBannerIdntfNo())) {
|
||||||
|
MainBanner existing = mainBannerMapper.selectBanner(form.getBannerIdntfNo());
|
||||||
|
if (existing == null) {
|
||||||
|
throw new IllegalArgumentException("수정할 배너를 찾을 수 없습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
MainBanner banner = toBanner(form);
|
||||||
|
banner.setBannerIdntfNo(existing.getBannerIdntfNo());
|
||||||
|
banner.setSortOrder(existing.getSortOrder());
|
||||||
|
mainBannerMapper.updateBanner(banner);
|
||||||
|
return existing.getBannerIdntfNo();
|
||||||
|
}
|
||||||
|
|
||||||
|
MainBanner banner = toBanner(form);
|
||||||
|
banner.setBannerIdntfNo(newIdentifier());
|
||||||
|
banner.setSortOrder(mainBannerMapper.selectMaxSortOrder() + 10);
|
||||||
|
mainBannerMapper.insertBanner(banner);
|
||||||
|
return banner.getBannerIdntfNo();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void delete(String bannerIdntfNo) {
|
||||||
|
mainBannerMapper.deleteBanner(bannerIdntfNo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void move(String bannerIdntfNo, String direction) {
|
||||||
|
MainBanner current = mainBannerMapper.selectBanner(bannerIdntfNo);
|
||||||
|
if (current == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MainBanner target = "up".equals(direction)
|
||||||
|
? mainBannerMapper.selectPreviousBanner(current.getSortOrder())
|
||||||
|
: mainBannerMapper.selectNextBanner(current.getSortOrder());
|
||||||
|
|
||||||
|
if (target == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mainBannerMapper.updateSortOrder(current.getBannerIdntfNo(), target.getSortOrder());
|
||||||
|
mainBannerMapper.updateSortOrder(target.getBannerIdntfNo(), current.getSortOrder());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String resolveImageUrl(String imageAtchFileIdntfNo, Integer imageFileSn, String imageUrl) {
|
||||||
|
if (StringUtils.hasText(imageAtchFileIdntfNo) && imageFileSn != null && imageFileSn > 0) {
|
||||||
|
return fileStorageService.toPublicUrl(imageAtchFileIdntfNo, imageFileSn);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.hasText(imageUrl)) {
|
||||||
|
return imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FALLBACK_IMAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MainBanner toBanner(MainBannerForm form) {
|
||||||
|
boolean hasUploadedImage = hasUploadedImage(form);
|
||||||
|
|
||||||
|
MainBanner banner = new MainBanner();
|
||||||
|
banner.setTitle(form.getTitle().trim());
|
||||||
|
banner.setContent(form.getContent().trim());
|
||||||
|
banner.setImageAtchFileIdntfNo(hasUploadedImage ? form.getImageAtchFileIdntfNo() : null);
|
||||||
|
banner.setImageFileSn(hasUploadedImage ? form.getImageFileSn() : null);
|
||||||
|
banner.setImageUrl(hasUploadedImage ? null : trimToNull(form.getImageUrl()));
|
||||||
|
banner.setUseYn("Y".equals(form.getUseYn()) ? "Y" : "N");
|
||||||
|
return banner;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MainBanner enrich(MainBanner banner) {
|
||||||
|
banner.setImageUrl(resolveImageUrl(
|
||||||
|
banner.getImageAtchFileIdntfNo(),
|
||||||
|
banner.getImageFileSn(),
|
||||||
|
banner.getImageUrl()
|
||||||
|
));
|
||||||
|
return banner;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasUploadedImage(MainBannerForm form) {
|
||||||
|
return StringUtils.hasText(form.getImageAtchFileIdntfNo())
|
||||||
|
&& form.getImageFileSn() != null
|
||||||
|
&& form.getImageFileSn() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String trimToNull(String value) {
|
||||||
|
if (!StringUtils.hasText(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String newIdentifier() {
|
||||||
|
return UUID.randomUUID().toString().replace("-", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -90,7 +90,7 @@ public class PortfolioService {
|
|||||||
item.setContent(form.getContent().trim());
|
item.setContent(form.getContent().trim());
|
||||||
item.setThumbnailAtchFileIdntfNo(thumbnailAtchFileIdntfNo);
|
item.setThumbnailAtchFileIdntfNo(thumbnailAtchFileIdntfNo);
|
||||||
item.setThumbnailFileSn(thumbnailFileSn);
|
item.setThumbnailFileSn(thumbnailFileSn);
|
||||||
item.setRegisteredAt(LocalDateTime.now());
|
item.setRegisteredAt(form.getRegisteredAt() == null ? LocalDateTime.now() : form.getRegisteredAt());
|
||||||
item.setRegisteredByUserIdntfNo(principal.getUserIdntfNo());
|
item.setRegisteredByUserIdntfNo(principal.getUserIdntfNo());
|
||||||
portfolioMapper.insertPortfolio(item);
|
portfolioMapper.insertPortfolio(item);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
package kr.iotdoor.boot.web;
|
||||||
|
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
import kr.iotdoor.boot.model.statistics.AccessLog;
|
||||||
|
import kr.iotdoor.boot.service.AccessStatisticsService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class AccessLogFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(AccessLogFilter.class);
|
||||||
|
|
||||||
|
private static final List<String> SKIP_PREFIXES = List.of(
|
||||||
|
"/resources/",
|
||||||
|
"/comn/file/view/",
|
||||||
|
"/comn/user/login/",
|
||||||
|
"/mfuc/",
|
||||||
|
"/mngr/",
|
||||||
|
"/actuator/",
|
||||||
|
"/error",
|
||||||
|
"/favicon"
|
||||||
|
);
|
||||||
|
|
||||||
|
private static final List<String> STATIC_EXTENSIONS = List.of(
|
||||||
|
".css",
|
||||||
|
".js",
|
||||||
|
".map",
|
||||||
|
".ico",
|
||||||
|
".png",
|
||||||
|
".jpg",
|
||||||
|
".jpeg",
|
||||||
|
".gif",
|
||||||
|
".svg",
|
||||||
|
".webp",
|
||||||
|
".woff",
|
||||||
|
".woff2",
|
||||||
|
".ttf",
|
||||||
|
".eot",
|
||||||
|
".otf"
|
||||||
|
);
|
||||||
|
|
||||||
|
private final AccessStatisticsService accessStatisticsService;
|
||||||
|
|
||||||
|
public AccessLogFilter(AccessStatisticsService accessStatisticsService) {
|
||||||
|
this.accessStatisticsService = accessStatisticsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||||
|
String path = getApplicationPath(request);
|
||||||
|
if (!StringUtils.hasText(path)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String method = request.getMethod();
|
||||||
|
if ("OPTIONS".equalsIgnoreCase(method)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String skipPrefix : SKIP_PREFIXES) {
|
||||||
|
if (path.startsWith(skipPrefix)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String lowerPath = path.toLowerCase(Locale.ROOT);
|
||||||
|
for (String staticExtension : STATIC_EXTENSIONS) {
|
||||||
|
if (lowerPath.endsWith(staticExtension)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
FilterChain filterChain
|
||||||
|
) throws ServletException, IOException {
|
||||||
|
HttpSession session = request.getSession();
|
||||||
|
String sessionId = session.getId();
|
||||||
|
|
||||||
|
try {
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
} finally {
|
||||||
|
if (!request.isAsyncStarted()) {
|
||||||
|
recordAccess(request, response, sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldNotFilterErrorDispatch() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recordAccess(HttpServletRequest request, HttpServletResponse response, String sessionId) {
|
||||||
|
try {
|
||||||
|
AccessLog accessLog = new AccessLog();
|
||||||
|
accessLog.setSessionId(limit(sessionId, 128));
|
||||||
|
accessLog.setRefererUrl(limit(request.getHeader("Referer"), 1000));
|
||||||
|
accessLog.setAccessIp(limit(resolveClientIp(request), 45));
|
||||||
|
accessLog.setHttpMethod(limit(request.getMethod(), 10));
|
||||||
|
accessLog.setRequestUri(limit(getApplicationPath(request), 500));
|
||||||
|
accessLog.setQueryString(limit(request.getQueryString(), 1000));
|
||||||
|
accessLog.setUserAgent(limit(request.getHeader("User-Agent"), 1000));
|
||||||
|
accessLog.setStatusCode(response.getStatus());
|
||||||
|
accessStatisticsService.record(accessLog);
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
LOGGER.warn("Failed to record access statistics.", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveClientIp(HttpServletRequest request) {
|
||||||
|
String forwardedFor = request.getHeader("X-Forwarded-For");
|
||||||
|
if (StringUtils.hasText(forwardedFor)) {
|
||||||
|
return forwardedFor.split(",")[0].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
String realIp = request.getHeader("X-Real-IP");
|
||||||
|
if (StringUtils.hasText(realIp)) {
|
||||||
|
return realIp.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return request.getRemoteAddr();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getApplicationPath(HttpServletRequest request) {
|
||||||
|
String requestUri = request.getRequestURI();
|
||||||
|
String contextPath = request.getContextPath();
|
||||||
|
if (StringUtils.hasText(contextPath) && requestUri.startsWith(contextPath)) {
|
||||||
|
return requestUri.substring(contextPath.length());
|
||||||
|
}
|
||||||
|
return requestUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String limit(String value, int maxLength) {
|
||||||
|
if (!StringUtils.hasText(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String trimmed = value.trim();
|
||||||
|
if (trimmed.length() <= maxLength) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmed.substring(0, maxLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package kr.iotdoor.boot.web;
|
||||||
|
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import kr.iotdoor.boot.model.banner.MainBannerForm;
|
||||||
|
import kr.iotdoor.boot.service.MainBannerService;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.validation.BindingResult;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class AdminMainBannerController {
|
||||||
|
|
||||||
|
private final MainBannerService mainBannerService;
|
||||||
|
|
||||||
|
public AdminMainBannerController(MainBannerService mainBannerService) {
|
||||||
|
this.mainBannerService = mainBannerService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/mfuc/banner/list.do")
|
||||||
|
public String bannerList(Model model) {
|
||||||
|
model.addAttribute("banners", mainBannerService.getAdminBanners());
|
||||||
|
model.addAttribute("adminMenu", "banner");
|
||||||
|
model.addAttribute("pageTitle", "배너 관리");
|
||||||
|
model.addAttribute("pageDescription", "메인 서비스 배너의 이미지, 제목, 내용, 노출 순서를 관리합니다.");
|
||||||
|
return "kr/iotdoor/boot/admin/banner/list";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/mfuc/banner/form.do")
|
||||||
|
public String bannerForm(
|
||||||
|
@RequestParam(name = "bannerIdntfNo", required = false) String bannerIdntfNo,
|
||||||
|
Model model
|
||||||
|
) {
|
||||||
|
MainBannerForm form = mainBannerService.getForm(bannerIdntfNo);
|
||||||
|
model.addAttribute("form", form);
|
||||||
|
model.addAttribute("currentImageUrl", currentImageUrl(form));
|
||||||
|
applyFormPage(model, form);
|
||||||
|
return "kr/iotdoor/boot/admin/banner/form";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/mfuc/banner/save.do")
|
||||||
|
public String saveBanner(
|
||||||
|
@Valid @ModelAttribute("form") MainBannerForm form,
|
||||||
|
BindingResult bindingResult,
|
||||||
|
@RequestParam(name = "useYn", required = false) String useYn,
|
||||||
|
Model model
|
||||||
|
) {
|
||||||
|
form.setUseYn("Y".equals(useYn) ? "Y" : "N");
|
||||||
|
|
||||||
|
if (!hasImage(form)) {
|
||||||
|
bindingResult.rejectValue("imageAtchFileIdntfNo", "required", "배너 이미지는 필수입니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bindingResult.hasErrors()) {
|
||||||
|
model.addAttribute("currentImageUrl", currentImageUrl(form));
|
||||||
|
applyFormPage(model, form);
|
||||||
|
return "kr/iotdoor/boot/admin/banner/form";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
mainBannerService.save(form);
|
||||||
|
return "redirect:/mfuc/banner/list.do?saved=1";
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
bindingResult.reject("saveError", exception.getMessage());
|
||||||
|
model.addAttribute("currentImageUrl", currentImageUrl(form));
|
||||||
|
applyFormPage(model, form);
|
||||||
|
return "kr/iotdoor/boot/admin/banner/form";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/mfuc/banner/delete.do")
|
||||||
|
public String deleteBanner(@RequestParam("bannerIdntfNo") String bannerIdntfNo) {
|
||||||
|
mainBannerService.delete(bannerIdntfNo);
|
||||||
|
return "redirect:/mfuc/banner/list.do?deleted=1";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/mfuc/banner/move.do")
|
||||||
|
public String moveBanner(
|
||||||
|
@RequestParam("bannerIdntfNo") String bannerIdntfNo,
|
||||||
|
@RequestParam("direction") String direction
|
||||||
|
) {
|
||||||
|
mainBannerService.move(bannerIdntfNo, direction);
|
||||||
|
return "redirect:/mfuc/banner/list.do?ordered=1";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyFormPage(Model model, MainBannerForm form) {
|
||||||
|
model.addAttribute("adminMenu", "banner");
|
||||||
|
model.addAttribute("pageTitle", StringUtils.hasText(form.getBannerIdntfNo()) ? "배너 수정" : "배너 등록");
|
||||||
|
model.addAttribute("pageDescription", "메인 서비스 영역에 노출할 배너 이미지와 문구를 입력합니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasImage(MainBannerForm form) {
|
||||||
|
return StringUtils.hasText(form.getImageUrl())
|
||||||
|
|| (
|
||||||
|
StringUtils.hasText(form.getImageAtchFileIdntfNo())
|
||||||
|
&& form.getImageFileSn() != null
|
||||||
|
&& form.getImageFileSn() > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String currentImageUrl(MainBannerForm form) {
|
||||||
|
return mainBannerService.resolveImageUrl(
|
||||||
|
form.getImageAtchFileIdntfNo(),
|
||||||
|
form.getImageFileSn(),
|
||||||
|
form.getImageUrl()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,6 +46,7 @@ public class AdminPortfolioController {
|
|||||||
form.setContent(portfolio.getContent());
|
form.setContent(portfolio.getContent());
|
||||||
form.setThumbnailAtchFileIdntfNo(portfolio.getThumbnailAtchFileIdntfNo());
|
form.setThumbnailAtchFileIdntfNo(portfolio.getThumbnailAtchFileIdntfNo());
|
||||||
form.setThumbnailFileSn(portfolio.getThumbnailFileSn());
|
form.setThumbnailFileSn(portfolio.getThumbnailFileSn());
|
||||||
|
form.setRegisteredAt(portfolio.getRegisteredAt());
|
||||||
model.addAttribute("currentThumbnailUrl", portfolio.getImageUrl());
|
model.addAttribute("currentThumbnailUrl", portfolio.getImageUrl());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package kr.iotdoor.boot.web;
|
||||||
|
|
||||||
|
import kr.iotdoor.boot.service.AccessStatisticsService;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class AdminStatisticsController {
|
||||||
|
|
||||||
|
private final AccessStatisticsService accessStatisticsService;
|
||||||
|
|
||||||
|
public AdminStatisticsController(AccessStatisticsService accessStatisticsService) {
|
||||||
|
this.accessStatisticsService = accessStatisticsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/mfuc/statistics/access.do")
|
||||||
|
public String accessStatistics(Model model) {
|
||||||
|
model.addAttribute("summary", accessStatisticsService.getSummary());
|
||||||
|
model.addAttribute("dailyStats", accessStatisticsService.getDailyStats(14));
|
||||||
|
model.addAttribute("topPages", accessStatisticsService.getTopPages(10));
|
||||||
|
model.addAttribute("topReferers", accessStatisticsService.getTopReferers(10));
|
||||||
|
model.addAttribute("recentLogs", accessStatisticsService.getRecentLogs(100));
|
||||||
|
model.addAttribute("adminMenu", "statistics");
|
||||||
|
model.addAttribute("adminSubMenu", "access");
|
||||||
|
model.addAttribute("pageTitle", "접속 통계");
|
||||||
|
model.addAttribute("pageDescription", "공개 페이지 접속 기록과 주요 유입 정보를 확인합니다.");
|
||||||
|
return "kr/iotdoor/boot/admin/statistics/access";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,29 @@
|
|||||||
package kr.iotdoor.boot.web;
|
package kr.iotdoor.boot.web;
|
||||||
|
|
||||||
import kr.iotdoor.boot.service.PortfolioService;
|
import kr.iotdoor.boot.service.PortfolioService;
|
||||||
|
import kr.iotdoor.boot.service.MainBannerService;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
public class PublicPageController {
|
public class PublicPageController {
|
||||||
|
|
||||||
|
private final MainBannerService mainBannerService;
|
||||||
private final PortfolioService portfolioService;
|
private final PortfolioService portfolioService;
|
||||||
|
|
||||||
public PublicPageController(PortfolioService portfolioService) {
|
public PublicPageController(MainBannerService mainBannerService, PortfolioService portfolioService) {
|
||||||
|
this.mainBannerService = mainBannerService;
|
||||||
this.portfolioService = portfolioService;
|
this.portfolioService = portfolioService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping({"/", "/index.do"})
|
@GetMapping({"/", "/index.do"})
|
||||||
public String home(Model model) {
|
public String home(Model model) {
|
||||||
model.addAttribute("mainYn", "Y");
|
model.addAttribute("mainYn", "Y");
|
||||||
|
model.addAttribute("mainBanners", mainBannerService.getPublicBanners());
|
||||||
return "kr/iotdoor/comn/site/mainPge";
|
return "kr/iotdoor/comn/site/mainPge";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +49,37 @@ public class PublicPageController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model.addAttribute("portfolio", portfolio);
|
model.addAttribute("portfolio", portfolio);
|
||||||
|
model.addAttribute("portfolioCount", portfolioService.count());
|
||||||
return "kr/iotdoor/cmty/portfolio/detail";
|
return "kr/iotdoor/cmty/portfolio/detail";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ResponseBody
|
||||||
|
@GetMapping("/portfolio/items.json")
|
||||||
|
public List<PortfolioListItemResponse> portfolioItems() {
|
||||||
|
return portfolioService.getPortfolioItems().stream()
|
||||||
|
.map(PortfolioListItemResponse::from)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PortfolioListItemResponse(
|
||||||
|
String portfolioIdntfNo,
|
||||||
|
String title,
|
||||||
|
String summary,
|
||||||
|
String imageUrl,
|
||||||
|
LocalDateTime registeredAt,
|
||||||
|
long viewCount,
|
||||||
|
String registeredByName
|
||||||
|
) {
|
||||||
|
public static PortfolioListItemResponse from(kr.iotdoor.boot.model.portfolio.PortfolioItem item) {
|
||||||
|
return new PortfolioListItemResponse(
|
||||||
|
item.getPortfolioIdntfNo(),
|
||||||
|
item.getTitle(),
|
||||||
|
item.getSummary(),
|
||||||
|
item.getImageUrl(),
|
||||||
|
item.getRegisteredAt(),
|
||||||
|
item.getViewCount(),
|
||||||
|
item.getRegisteredByName()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ spring:
|
|||||||
sql:
|
sql:
|
||||||
init:
|
init:
|
||||||
mode: always
|
mode: always
|
||||||
|
encoding: UTF-8
|
||||||
jackson:
|
jackson:
|
||||||
time-zone: Asia/Seoul
|
time-zone: Asia/Seoul
|
||||||
mvc:
|
mvc:
|
||||||
|
|||||||
@@ -66,6 +66,193 @@ CREATE TABLE IF NOT EXISTS TN_CONTACT_INQRY (
|
|||||||
PRIMARY KEY (CONTACT_IDNTF_NO)
|
PRIMARY KEY (CONTACT_IDNTF_NO)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS TN_ACCESS_LOG (
|
||||||
|
ACCESS_LOG_IDNTF_NO VARCHAR(32) NOT NULL,
|
||||||
|
SESSION_ID VARCHAR(128) NULL,
|
||||||
|
REFERER_URL VARCHAR(1000) NULL,
|
||||||
|
ACCESS_IP VARCHAR(45) NULL,
|
||||||
|
HTTP_METHOD VARCHAR(10) NOT NULL,
|
||||||
|
REQUEST_URI VARCHAR(500) NOT NULL,
|
||||||
|
QUERY_STRING VARCHAR(1000) NULL,
|
||||||
|
USER_AGENT VARCHAR(1000) NULL,
|
||||||
|
STATUS_CD INT NULL,
|
||||||
|
REG_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (ACCESS_LOG_IDNTF_NO)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS TN_MAIN_BANNER (
|
||||||
|
BANNER_IDNTF_NO VARCHAR(32) NOT NULL,
|
||||||
|
TTL VARCHAR(200) NOT NULL,
|
||||||
|
CN TEXT NULL,
|
||||||
|
IMAGE_ATCH_FILE_IDNTF_NO VARCHAR(32) NULL,
|
||||||
|
IMAGE_FILE_SN INT NULL,
|
||||||
|
IMAGE_URL VARCHAR(500) NULL,
|
||||||
|
SORT_ORDR INT NOT NULL DEFAULT 0,
|
||||||
|
USE_YN CHAR(1) NOT NULL DEFAULT 'Y',
|
||||||
|
DEL_YN CHAR(1) NOT NULL DEFAULT 'N',
|
||||||
|
REG_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UPD_DT DATETIME NULL,
|
||||||
|
PRIMARY KEY (BANNER_IDNTF_NO),
|
||||||
|
CONSTRAINT FK_TN_MAIN_BANNER__IMAGE FOREIGN KEY (IMAGE_ATCH_FILE_IDNTF_NO)
|
||||||
|
REFERENCES TN_ATCH_FILE (ATCH_FILE_IDNTF_NO)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS IDX_TN_ATCH_FILE_DTL__PATH ON TN_ATCH_FILE_DTL (FILE_STRG_PATH_NM);
|
CREATE INDEX IF NOT EXISTS IDX_TN_ATCH_FILE_DTL__PATH ON TN_ATCH_FILE_DTL (FILE_STRG_PATH_NM);
|
||||||
CREATE INDEX IF NOT EXISTS IDX_TN_PORTFOLIO__REG_DT ON TN_PORTFOLIO (REG_DT);
|
CREATE INDEX IF NOT EXISTS IDX_TN_PORTFOLIO__REG_DT ON TN_PORTFOLIO (REG_DT);
|
||||||
CREATE INDEX IF NOT EXISTS IDX_TN_CONTACT_INQRY__SUBMIT_DT ON TN_CONTACT_INQRY (SUBMIT_DT);
|
CREATE INDEX IF NOT EXISTS IDX_TN_CONTACT_INQRY__SUBMIT_DT ON TN_CONTACT_INQRY (SUBMIT_DT);
|
||||||
|
CREATE INDEX IF NOT EXISTS IDX_TN_ACCESS_LOG__REG_DT ON TN_ACCESS_LOG (REG_DT);
|
||||||
|
CREATE INDEX IF NOT EXISTS IDX_TN_ACCESS_LOG__ACCESS_IP ON TN_ACCESS_LOG (ACCESS_IP);
|
||||||
|
CREATE INDEX IF NOT EXISTS IDX_TN_ACCESS_LOG__SESSION_ID ON TN_ACCESS_LOG (SESSION_ID);
|
||||||
|
CREATE INDEX IF NOT EXISTS IDX_TN_ACCESS_LOG__REQUEST_URI ON TN_ACCESS_LOG (REQUEST_URI);
|
||||||
|
CREATE INDEX IF NOT EXISTS IDX_TN_MAIN_BANNER__SORT ON TN_MAIN_BANNER (SORT_ORDR);
|
||||||
|
CREATE INDEX IF NOT EXISTS IDX_TN_MAIN_BANNER__USE_DEL ON TN_MAIN_BANNER (USE_YN, DEL_YN);
|
||||||
|
|
||||||
|
INSERT INTO TN_MAIN_BANNER (
|
||||||
|
BANNER_IDNTF_NO, TTL, CN, IMAGE_URL, SORT_ORDR, USE_YN, DEL_YN, REG_DT
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'00000000000000000000000000000001',
|
||||||
|
'슬라이딩자동문',
|
||||||
|
'슬라이드 오토도어를 현장 조건에 맞춰 시공합니다.',
|
||||||
|
'/resources/kr/iotdoor/comn/site/layout/image/template/img-600x400-1.jpg',
|
||||||
|
10,
|
||||||
|
'Y',
|
||||||
|
'N',
|
||||||
|
CURRENT_TIMESTAMP
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM TN_MAIN_BANNER WHERE BANNER_IDNTF_NO = '00000000000000000000000000000001'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO TN_MAIN_BANNER (
|
||||||
|
BANNER_IDNTF_NO, TTL, CN, IMAGE_URL, SORT_ORDR, USE_YN, DEL_YN, REG_DT
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'00000000000000000000000000000002',
|
||||||
|
'산업용자동문',
|
||||||
|
'공장, 물류 현장에 필요한 산업용 자동문을 제공합니다.',
|
||||||
|
'/resources/kr/iotdoor/comn/site/layout/image/template/img-600x400-2.jpg',
|
||||||
|
20,
|
||||||
|
'Y',
|
||||||
|
'N',
|
||||||
|
CURRENT_TIMESTAMP
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM TN_MAIN_BANNER WHERE BANNER_IDNTF_NO = '00000000000000000000000000000002'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO TN_MAIN_BANNER (
|
||||||
|
BANNER_IDNTF_NO, TTL, CN, IMAGE_URL, SORT_ORDR, USE_YN, DEL_YN, REG_DT
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'00000000000000000000000000000003',
|
||||||
|
'고급산업용자동문',
|
||||||
|
'정밀 제어와 안정성이 필요한 고급 산업 현장에 대응합니다.',
|
||||||
|
'/resources/kr/iotdoor/comn/site/layout/image/template/img-600x400-3.jpg',
|
||||||
|
30,
|
||||||
|
'Y',
|
||||||
|
'N',
|
||||||
|
CURRENT_TIMESTAMP
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM TN_MAIN_BANNER WHERE BANNER_IDNTF_NO = '00000000000000000000000000000003'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO TN_MAIN_BANNER (
|
||||||
|
BANNER_IDNTF_NO, TTL, CN, IMAGE_URL, SORT_ORDR, USE_YN, DEL_YN, REG_DT
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'00000000000000000000000000000004',
|
||||||
|
'내풍압고속셔터',
|
||||||
|
'바람과 사용 빈도가 높은 공간에 적합한 고속 셔터를 시공합니다.',
|
||||||
|
'/resources/kr/iotdoor/comn/site/layout/image/template/img-600x400-4.jpg',
|
||||||
|
40,
|
||||||
|
'Y',
|
||||||
|
'N',
|
||||||
|
CURRENT_TIMESTAMP
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM TN_MAIN_BANNER WHERE BANNER_IDNTF_NO = '00000000000000000000000000000004'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO TN_MAIN_BANNER (
|
||||||
|
BANNER_IDNTF_NO, TTL, CN, IMAGE_URL, SORT_ORDR, USE_YN, DEL_YN, REG_DT
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'00000000000000000000000000000005',
|
||||||
|
'오버헤드도어',
|
||||||
|
'창고와 산업 시설에 맞는 오버헤드도어를 설치합니다.',
|
||||||
|
'/resources/kr/iotdoor/comn/site/layout/image/template/img-600x400-5.jpg?1',
|
||||||
|
50,
|
||||||
|
'Y',
|
||||||
|
'N',
|
||||||
|
CURRENT_TIMESTAMP
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM TN_MAIN_BANNER WHERE BANNER_IDNTF_NO = '00000000000000000000000000000005'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO TN_MAIN_BANNER (
|
||||||
|
BANNER_IDNTF_NO, TTL, CN, IMAGE_URL, SORT_ORDR, USE_YN, DEL_YN, REG_DT
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'00000000000000000000000000000006',
|
||||||
|
'방화용슬라이딩자동문',
|
||||||
|
'방화 성능이 필요한 출입 공간에 맞춘 자동문 솔루션입니다.',
|
||||||
|
'/resources/kr/iotdoor/comn/site/layout/image/template/img-600x400-6.jpg?1?1',
|
||||||
|
60,
|
||||||
|
'Y',
|
||||||
|
'N',
|
||||||
|
CURRENT_TIMESTAMP
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM TN_MAIN_BANNER WHERE BANNER_IDNTF_NO = '00000000000000000000000000000006'
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE TN_MAIN_BANNER
|
||||||
|
SET
|
||||||
|
TTL = '슬라이딩자동문',
|
||||||
|
CN = '슬라이드 오토도어를 현장 조건에 맞춰 시공합니다.',
|
||||||
|
IMAGE_URL = COALESCE(IMAGE_URL, '/resources/kr/iotdoor/comn/site/layout/image/template/img-600x400-1.jpg'),
|
||||||
|
UPD_DT = CURRENT_TIMESTAMP
|
||||||
|
WHERE BANNER_IDNTF_NO = '00000000000000000000000000000001'
|
||||||
|
AND (TTL LIKE '%�%' OR CN LIKE '%�%');
|
||||||
|
|
||||||
|
UPDATE TN_MAIN_BANNER
|
||||||
|
SET
|
||||||
|
TTL = '산업용자동문',
|
||||||
|
CN = '공장, 물류 현장에 필요한 산업용 자동문을 제공합니다.',
|
||||||
|
IMAGE_URL = COALESCE(IMAGE_URL, '/resources/kr/iotdoor/comn/site/layout/image/template/img-600x400-2.jpg'),
|
||||||
|
UPD_DT = CURRENT_TIMESTAMP
|
||||||
|
WHERE BANNER_IDNTF_NO = '00000000000000000000000000000002'
|
||||||
|
AND (TTL LIKE '%�%' OR CN LIKE '%�%');
|
||||||
|
|
||||||
|
UPDATE TN_MAIN_BANNER
|
||||||
|
SET
|
||||||
|
TTL = '고급산업용자동문',
|
||||||
|
CN = '정밀 제어와 안정성이 필요한 고급 산업 현장에 대응합니다.',
|
||||||
|
IMAGE_URL = COALESCE(IMAGE_URL, '/resources/kr/iotdoor/comn/site/layout/image/template/img-600x400-3.jpg'),
|
||||||
|
UPD_DT = CURRENT_TIMESTAMP
|
||||||
|
WHERE BANNER_IDNTF_NO = '00000000000000000000000000000003'
|
||||||
|
AND (TTL LIKE '%�%' OR CN LIKE '%�%');
|
||||||
|
|
||||||
|
UPDATE TN_MAIN_BANNER
|
||||||
|
SET
|
||||||
|
TTL = '내풍압고속셔터',
|
||||||
|
CN = '바람과 사용 빈도가 높은 공간에 적합한 고속 셔터를 시공합니다.',
|
||||||
|
IMAGE_URL = COALESCE(IMAGE_URL, '/resources/kr/iotdoor/comn/site/layout/image/template/img-600x400-4.jpg'),
|
||||||
|
UPD_DT = CURRENT_TIMESTAMP
|
||||||
|
WHERE BANNER_IDNTF_NO = '00000000000000000000000000000004'
|
||||||
|
AND (TTL LIKE '%�%' OR CN LIKE '%�%');
|
||||||
|
|
||||||
|
UPDATE TN_MAIN_BANNER
|
||||||
|
SET
|
||||||
|
TTL = '오버헤드도어',
|
||||||
|
CN = '창고와 산업 시설에 맞는 오버헤드도어를 설치합니다.',
|
||||||
|
IMAGE_URL = COALESCE(IMAGE_URL, '/resources/kr/iotdoor/comn/site/layout/image/template/img-600x400-5.jpg?1'),
|
||||||
|
UPD_DT = CURRENT_TIMESTAMP
|
||||||
|
WHERE BANNER_IDNTF_NO = '00000000000000000000000000000005'
|
||||||
|
AND (TTL LIKE '%�%' OR CN LIKE '%�%');
|
||||||
|
|
||||||
|
UPDATE TN_MAIN_BANNER
|
||||||
|
SET
|
||||||
|
TTL = '방화용슬라이딩자동문',
|
||||||
|
CN = '방화 성능이 필요한 출입 공간에 맞춘 자동문 솔루션입니다.',
|
||||||
|
IMAGE_URL = COALESCE(IMAGE_URL, '/resources/kr/iotdoor/comn/site/layout/image/template/img-600x400-6.jpg?1?1'),
|
||||||
|
UPD_DT = CURRENT_TIMESTAMP
|
||||||
|
WHERE BANNER_IDNTF_NO = '00000000000000000000000000000006'
|
||||||
|
AND (TTL LIKE '%�%' OR CN LIKE '%�%');
|
||||||
|
|||||||
@@ -3,10 +3,15 @@ body {
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--iotdoor-admin-header-height: 104px;
|
||||||
|
}
|
||||||
|
|
||||||
.iotdoor-admin-shell {
|
.iotdoor-admin-shell {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
padding-top: var(--iotdoor-admin-header-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
.iotdoor-admin-sidebar {
|
.iotdoor-admin-sidebar {
|
||||||
@@ -14,6 +19,18 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.iotdoor-admin-header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
min-height: var(--iotdoor-admin-header-height);
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.06);
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
|
||||||
.iotdoor-admin-main {
|
.iotdoor-admin-main {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -21,6 +38,10 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
@media (min-width: 1280px) {
|
||||||
|
.iotdoor-admin-shell {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.iotdoor-admin-sidebar {
|
.iotdoor-admin-sidebar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -33,6 +54,11 @@ body {
|
|||||||
.iotdoor-admin-main {
|
.iotdoor-admin-main {
|
||||||
margin-left: 290px;
|
margin-left: 290px;
|
||||||
width: calc(100% - 290px);
|
width: calc(100% - 290px);
|
||||||
|
padding-top: var(--iotdoor-admin-header-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-admin-header {
|
||||||
|
left: 290px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,10 +89,23 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.iotdoor-editor-surface {
|
.iotdoor-editor-surface {
|
||||||
|
position: relative;
|
||||||
border: 1px solid #d0d5dd;
|
border: 1px solid #d0d5dd;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-editor-surface.is-dragging-image {
|
||||||
|
border-color: #465fff;
|
||||||
|
background: #f8fbff;
|
||||||
|
box-shadow: 0 0 0 4px rgba(70, 95, 255, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-editor-surface.is-uploading-image,
|
||||||
|
.iotdoor-editor-surface.is-uploading-image .ProseMirror {
|
||||||
|
cursor: progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
.iotdoor-editor-surface .ProseMirror {
|
.iotdoor-editor-surface .ProseMirror {
|
||||||
@@ -121,33 +160,159 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.iotdoor-editor-image-node {
|
.iotdoor-editor-image-node {
|
||||||
|
display: table;
|
||||||
|
max-width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 1.25rem 0;
|
margin: 1.25rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.iotdoor-editor-image-node.is-representative::before {
|
.iotdoor-editor-image-node.is-representative::before {
|
||||||
content: "";
|
content: "✓ 대표";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
top: 0.75rem;
|
||||||
border-radius: 1rem;
|
left: 0.75rem;
|
||||||
background: linear-gradient(180deg, rgba(70, 95, 255, 0.18), rgba(22, 25, 80, 0.3));
|
z-index: 1;
|
||||||
|
padding: 0.45rem 0.7rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
background: #03c75a;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.iotdoor-editor-image-node.is-representative::after {
|
.iotdoor-editor-image-node.is-representative::after {
|
||||||
content: "대표 이미지";
|
content: none;
|
||||||
position: absolute;
|
|
||||||
top: 1rem;
|
|
||||||
right: 1rem;
|
|
||||||
padding: 0.5rem 0.875rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(16, 24, 40, 0.85);
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.iotdoor-editor-image-node.ProseMirror-selectednode img {
|
.iotdoor-editor-image-node.ProseMirror-selectednode img {
|
||||||
box-shadow: 0 0 0 4px rgba(70, 95, 255, 0.18);
|
box-shadow: 0 0 0 4px rgba(70, 95, 255, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.iotdoor-access-log-table {
|
||||||
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 1180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-access-log-table th,
|
||||||
|
.iotdoor-access-log-table td {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-access-log-table .access-log-col-time {
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-access-log-table .access-log-col-ip {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-access-log-table .access-log-col-request {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-access-log-table .access-log-col-referer {
|
||||||
|
width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-access-log-table .access-log-col-session {
|
||||||
|
width: 190px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-access-log-table .access-log-col-status {
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-access-log-table .access-log-col-agent {
|
||||||
|
width: 230px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-access-log-text {
|
||||||
|
display: block;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
white-space: normal;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-access-log-mono {
|
||||||
|
font-family: Consolas, "Courier New", monospace;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-banner-table {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 980px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-banner-list-content {
|
||||||
|
max-width: 520px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-banner-form-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-portfolio-table {
|
||||||
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 1040px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-portfolio-table th,
|
||||||
|
.iotdoor-portfolio-table td {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-portfolio-table .portfolio-col-image {
|
||||||
|
width: 170px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-portfolio-table .portfolio-col-title {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-portfolio-table .portfolio-col-date {
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-portfolio-table .portfolio-col-author {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-portfolio-table .portfolio-col-view {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-portfolio-table .portfolio-col-action {
|
||||||
|
width: 92px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-portfolio-table th {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-portfolio-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iotdoor-portfolio-actions a,
|
||||||
|
.iotdoor-portfolio-actions button {
|
||||||
|
min-width: 64px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.iotdoor-banner-form-grid {
|
||||||
|
grid-template-columns: 360px 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,344 @@
|
|||||||
|
.portfolio-post-list-panel {
|
||||||
|
background: #ffffff;
|
||||||
|
color: #202124;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-list-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0 0 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-list-summary {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.35rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-list-summary a {
|
||||||
|
color: #111111;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-list-summary span {
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-list-toggle {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #111111;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-list-toggle:hover,
|
||||||
|
.portfolio-post-list-toggle:focus-visible {
|
||||||
|
color: #1f5aa0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-list-body {
|
||||||
|
border-top: 1px solid #d5d9df;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-list-grid {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-list-grid.ag-theme-quartz {
|
||||||
|
--ag-font-family: "Noto Sans KR", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
--ag-font-size: 14px;
|
||||||
|
--ag-foreground-color: #111111;
|
||||||
|
--ag-secondary-foreground-color: #94979d;
|
||||||
|
--ag-header-background-color: #ffffff;
|
||||||
|
--ag-header-foreground-color: #8c9097;
|
||||||
|
--ag-row-hover-color: #fafafa;
|
||||||
|
--ag-selected-row-background-color: #ffffff;
|
||||||
|
--ag-border-color: transparent;
|
||||||
|
--ag-row-border-color: #e6e6e6;
|
||||||
|
--ag-wrapper-border-radius: 0;
|
||||||
|
--ag-cell-horizontal-padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-list-grid .ag-root-wrapper,
|
||||||
|
.portfolio-post-list-grid .ag-root,
|
||||||
|
.portfolio-post-list-grid .ag-body-viewport,
|
||||||
|
.portfolio-post-list-grid .ag-center-cols-viewport {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-list-grid .ag-header {
|
||||||
|
border-bottom: 1px solid #9da3ad;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-list-grid .ag-header-cell {
|
||||||
|
padding-inline: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-list-grid .ag-header-cell-label {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-number-header .ag-header-cell-label {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-date-header .ag-header-cell-label {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-list-grid .ag-header-cell-resize,
|
||||||
|
.portfolio-post-list-grid .ag-sort-indicator-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-list-grid .ag-row {
|
||||||
|
border-bottom: 1px solid #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-list-grid .ag-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 0;
|
||||||
|
padding-inline: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-list-grid .ag-row-current-post {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-number-cell {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-date-cell {
|
||||||
|
justify-content: center;
|
||||||
|
color: #111111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-title-link {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #111111;
|
||||||
|
text-decoration: none;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
vertical-align: middle;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-title-link:hover {
|
||||||
|
color: #111111;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-list-footer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
||||||
|
align-items: start;
|
||||||
|
gap: 1rem;
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-list-footer-side {
|
||||||
|
min-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-page-size-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-manage-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 32px;
|
||||||
|
border: 1px solid #d8d8d8;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #333333;
|
||||||
|
padding: 0.35rem 0.85rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-manage-link:hover {
|
||||||
|
border-color: #aaaaaa;
|
||||||
|
color: #111111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
min-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-page-button {
|
||||||
|
min-width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #111111;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-page-button:hover,
|
||||||
|
.portfolio-post-page-button:focus-visible {
|
||||||
|
color: #1f5aa0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-page-button.is-active {
|
||||||
|
border: 1px solid #dddddd;
|
||||||
|
background: #ffffff;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-page-size {
|
||||||
|
min-width: 106px;
|
||||||
|
height: 32px;
|
||||||
|
border: 1px solid #d8d8d8;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #111111;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: 0 2rem 0 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 575.98px) {
|
||||||
|
.portfolio-post-list-bar {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-list-toggle {
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-list-footer {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-post-page-size-wrap {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-frame-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 520px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 0 45px rgba(0, 0, 0, 0.08);
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-frame-card:hover {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-frame-card:focus-visible {
|
||||||
|
outline: 3px solid rgba(31, 90, 160, 0.35);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-frame-image-link {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 285px;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f8f9fa;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-frame-image {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 285px;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.35s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-frame-card:hover .portfolio-frame-image,
|
||||||
|
.portfolio-frame-card:focus-visible .portfolio-frame-image {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-frame-body {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-frame-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
color: #667085;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-frame-title {
|
||||||
|
display: -webkit-box;
|
||||||
|
min-height: 3.1rem;
|
||||||
|
margin: 0 0 0.9rem;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #101828;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.25;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-frame-card:hover .portfolio-frame-title,
|
||||||
|
.portfolio-frame-card:focus-visible .portfolio-frame-title {
|
||||||
|
color: #1f5aa0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-frame-summary {
|
||||||
|
display: -webkit-box;
|
||||||
|
min-height: 5.4rem;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #667085;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.portfolio-frame-card {
|
||||||
|
height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-frame-image-link {
|
||||||
|
flex-basis: 265px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-frame-image {
|
||||||
|
height: 265px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,3 +14,89 @@
|
|||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
box-shadow: 0 5px 6px rgba(0, 0, 0, .1019607843);
|
box-shadow: 0 5px 6px rgba(0, 0, 0, .1019607843);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.portfolio-content {
|
||||||
|
color: #1d2939;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-content p {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-content h1,
|
||||||
|
.portfolio-content h2,
|
||||||
|
.portfolio-content h3 {
|
||||||
|
color: #101828;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 1.5rem 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-content h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-content h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-content h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-content blockquote {
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-left: 4px solid #9cb9ff;
|
||||||
|
background: #f9fafb;
|
||||||
|
color: #475467;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-content ul,
|
||||||
|
.portfolio-content ol {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-content img {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
margin: 1.25rem 0;
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-service-card {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-service-card-image {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 3 / 2;
|
||||||
|
height: auto;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-service-card-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 210px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-service-card-body h4 {
|
||||||
|
word-break: keep-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-service-card-content {
|
||||||
|
flex: 1;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
word-break: keep-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-service-card-link {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,25 @@
|
|||||||
spinner();
|
spinner();
|
||||||
|
|
||||||
|
|
||||||
|
// Mobile navbar fallback
|
||||||
|
$(document).on('click', '.navbar-toggler[data-bs-target]', function () {
|
||||||
|
if (window.bootstrap && window.bootstrap.Collapse && window.bootstrap.Collapse.DATA_KEY === 'bs.collapse') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetSelector = $(this).attr('data-bs-target');
|
||||||
|
var $target = targetSelector ? $(targetSelector) : $();
|
||||||
|
|
||||||
|
if (!$target.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isOpen = $target.hasClass('show');
|
||||||
|
$target.toggleClass('show', !isOpen);
|
||||||
|
$(this).attr('aria-expanded', String(!isOpen));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// Initiate the wowjs
|
// Initiate the wowjs
|
||||||
new WOW().init();
|
new WOW().init();
|
||||||
|
|
||||||
@@ -95,4 +114,3 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
})(jQuery);
|
})(jQuery);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko"
|
||||||
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
|
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||||
|
layout:decorate="~{kr/iotdoor/boot/admin/layout/adminLayout}">
|
||||||
|
<body>
|
||||||
|
<div layout:fragment="content" class="space-y-6">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-semibold text-gray-800"
|
||||||
|
th:text="${form.bannerIdntfNo != null and !#strings.isEmpty(form.bannerIdntfNo) ? '배너 수정' : '배너 등록'}">배너 등록</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">메인 서비스 영역에 노출할 이미지, 제목, 내용을 입력합니다.</p>
|
||||||
|
</div>
|
||||||
|
<a class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
|
||||||
|
th:href="@{/mfuc/banner/list.do}">
|
||||||
|
목록으로
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="rounded-2xl border border-gray-200 bg-white">
|
||||||
|
<div class="border-b border-gray-100 px-6 py-5">
|
||||||
|
<h3 class="text-base font-medium text-gray-800">배너 정보</h3>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 sm:p-6">
|
||||||
|
<form id="mainBannerForm"
|
||||||
|
method="post"
|
||||||
|
th:action="@{/mfuc/banner/save.do}"
|
||||||
|
th:object="${form}"
|
||||||
|
th:attr="data-csrf-header=${_csrf.headerName},
|
||||||
|
data-csrf-token=${_csrf.token},
|
||||||
|
data-current-image-url=${currentImageUrl}">
|
||||||
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||||
|
<input type="hidden" th:field="*{bannerIdntfNo}">
|
||||||
|
<input type="hidden" th:field="*{imageAtchFileIdntfNo}" id="imageAtchFileIdntfNo">
|
||||||
|
<input type="hidden" th:field="*{imageFileSn}" id="imageFileSn">
|
||||||
|
<input type="hidden" th:field="*{imageUrl}" id="imageUrl">
|
||||||
|
|
||||||
|
<div class="iotdoor-banner-form-grid">
|
||||||
|
<div>
|
||||||
|
<label class="mb-2 block text-sm font-medium text-gray-700">배너 이미지</label>
|
||||||
|
<div class="rounded-2xl border border-gray-200 bg-gray-50 p-4">
|
||||||
|
<img id="bannerImagePreview"
|
||||||
|
class="aspect-4/3 w-full rounded-xl object-cover"
|
||||||
|
th:src="${currentImageUrl}"
|
||||||
|
alt="배너 이미지 미리보기">
|
||||||
|
<input id="bannerImageFile" type="file" accept="image/*" hidden>
|
||||||
|
<button id="bannerImageUploadButton"
|
||||||
|
class="mt-4 inline-flex w-full items-center justify-center rounded-lg bg-brand-500 px-4 py-2 text-sm font-medium text-white hover:bg-brand-600"
|
||||||
|
type="button">
|
||||||
|
이미지 업로드
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-sm text-error-600" th:if="${#fields.hasErrors('imageAtchFileIdntfNo')}" th:errors="*{imageAtchFileIdntfNo}">이미지 오류</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label class="mb-2 block text-sm font-medium text-gray-700" for="title">제목</label>
|
||||||
|
<input class="h-11 w-full rounded-lg border border-gray-300 bg-white px-4 text-sm text-gray-800 outline-none transition focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10"
|
||||||
|
id="title"
|
||||||
|
th:field="*{title}"
|
||||||
|
placeholder="배너 제목을 입력해주세요.">
|
||||||
|
<div class="mt-2 text-sm text-error-600" th:if="${#fields.hasErrors('title')}" th:errors="*{title}">제목 오류</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="mb-2 block text-sm font-medium text-gray-700" for="content">내용</label>
|
||||||
|
<textarea class="w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-sm leading-6 text-gray-800 outline-none transition focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10"
|
||||||
|
style="min-height: 160px;"
|
||||||
|
id="content"
|
||||||
|
th:field="*{content}"
|
||||||
|
placeholder="배너 내용을 입력해주세요."></textarea>
|
||||||
|
<div class="mt-2 text-sm text-error-600" th:if="${#fields.hasErrors('content')}" th:errors="*{content}">내용 오류</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="inline-flex items-center gap-3 text-sm font-medium text-gray-700">
|
||||||
|
<input class="size-5 rounded border-gray-300 text-brand-500"
|
||||||
|
type="checkbox"
|
||||||
|
th:field="*{useYn}"
|
||||||
|
value="Y">
|
||||||
|
메인에 노출
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-sm text-error-600" th:if="${#fields.hasGlobalErrors()}">
|
||||||
|
<span th:each="error : ${#fields.globalErrors()}" th:text="${error}">오류 메시지</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-3">
|
||||||
|
<button class="inline-flex items-center gap-2 rounded-lg bg-brand-500 px-4 py-2 text-sm font-medium text-white hover:bg-brand-600"
|
||||||
|
type="submit">
|
||||||
|
저장
|
||||||
|
</button>
|
||||||
|
<a class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
|
||||||
|
th:href="@{/mfuc/banner/list.do}">
|
||||||
|
취소
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<th:block layout:fragment="pageScripts">
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const form = document.getElementById('mainBannerForm');
|
||||||
|
const uploadButton = document.getElementById('bannerImageUploadButton');
|
||||||
|
const fileInput = document.getElementById('bannerImageFile');
|
||||||
|
const preview = document.getElementById('bannerImagePreview');
|
||||||
|
const atchFileIdInput = document.getElementById('imageAtchFileIdntfNo');
|
||||||
|
const fileSnInput = document.getElementById('imageFileSn');
|
||||||
|
const imageUrlInput = document.getElementById('imageUrl');
|
||||||
|
|
||||||
|
if (!form || !uploadButton || !fileInput || !preview || !atchFileIdInput || !fileSnInput || !imageUrlInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadButton.addEventListener('click', function () {
|
||||||
|
fileInput.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
fileInput.addEventListener('change', async function () {
|
||||||
|
if (!fileInput.files || fileInput.files.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
window.alert('이미지 파일만 업로드할 수 있습니다.');
|
||||||
|
fileInput.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('directory', 'banner');
|
||||||
|
formData.append('description', 'main-banner-image');
|
||||||
|
|
||||||
|
const headers = {};
|
||||||
|
const csrfHeader = form.dataset.csrfHeader;
|
||||||
|
const csrfToken = form.dataset.csrfToken;
|
||||||
|
if (csrfHeader && csrfToken) {
|
||||||
|
headers[csrfHeader] = csrfToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadButton.disabled = true;
|
||||||
|
uploadButton.textContent = '업로드 중';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/mfuc/file/upload.do', {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('이미지 업로드에 실패했습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
atchFileIdInput.value = result.atchFileIdntfNo || '';
|
||||||
|
fileSnInput.value = result.fileSn || '';
|
||||||
|
imageUrlInput.value = result.url || '';
|
||||||
|
preview.src = result.url;
|
||||||
|
} catch (error) {
|
||||||
|
window.alert(error.message || '이미지 업로드에 실패했습니다.');
|
||||||
|
} finally {
|
||||||
|
uploadButton.disabled = false;
|
||||||
|
uploadButton.textContent = '이미지 업로드';
|
||||||
|
fileInput.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</th:block>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko"
|
||||||
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
|
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||||
|
layout:decorate="~{kr/iotdoor/boot/admin/layout/adminLayout}">
|
||||||
|
<body>
|
||||||
|
<div layout:fragment="content" class="space-y-6">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-semibold text-gray-800">배너관리</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">메인 서비스 배너의 노출 순서와 내용을 관리합니다.</p>
|
||||||
|
</div>
|
||||||
|
<a class="inline-flex items-center gap-2 rounded-lg bg-brand-500 px-4 py-2 text-sm font-medium text-white hover:bg-brand-600"
|
||||||
|
th:href="@{/mfuc/banner/form.do}">
|
||||||
|
새 배너 등록
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-success-100 bg-success-50 px-4 py-3 text-sm text-success-700" th:if="${param.saved}">
|
||||||
|
배너가 저장되었습니다.
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border border-success-100 bg-success-50 px-4 py-3 text-sm text-success-700" th:if="${param.deleted}">
|
||||||
|
배너가 삭제되었습니다.
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border border-success-100 bg-success-50 px-4 py-3 text-sm text-success-700" th:if="${param.ordered}">
|
||||||
|
배너 순서가 변경되었습니다.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="rounded-2xl border border-gray-200 bg-white">
|
||||||
|
<div class="border-b border-gray-100 px-6 py-5">
|
||||||
|
<h3 class="text-base font-medium text-gray-800">등록 목록</h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">위/아래 버튼으로 메인 노출 순서를 변경할 수 있습니다.</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 sm:p-6">
|
||||||
|
<div th:if="${#lists.isEmpty(banners)}" class="rounded-xl bg-gray-50 px-4 py-6 text-sm text-gray-500">
|
||||||
|
아직 등록된 배너가 없습니다.
|
||||||
|
</div>
|
||||||
|
<div th:unless="${#lists.isEmpty(banners)}" class="overflow-x-auto">
|
||||||
|
<table class="iotdoor-banner-table text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-gray-100 text-left text-gray-500">
|
||||||
|
<th class="px-3 py-3">순서</th>
|
||||||
|
<th class="px-3 py-3">이미지</th>
|
||||||
|
<th class="px-3 py-3">제목/내용</th>
|
||||||
|
<th class="px-3 py-3">노출</th>
|
||||||
|
<th class="px-3 py-3">등록일시</th>
|
||||||
|
<th class="px-3 py-3">관리</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="border-b border-gray-100 align-top last:border-0"
|
||||||
|
th:each="banner, stat : ${banners}">
|
||||||
|
<td class="px-3 py-4" style="width: 92px;">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<form method="post" th:action="@{/mfuc/banner/move.do}">
|
||||||
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||||
|
<input type="hidden" name="bannerIdntfNo" th:value="${banner.bannerIdntfNo}">
|
||||||
|
<input type="hidden" name="direction" value="up">
|
||||||
|
<button class="inline-flex w-full items-center justify-center rounded-lg border border-gray-200 px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
|
type="submit"
|
||||||
|
th:disabled="${stat.first}">
|
||||||
|
위
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" th:action="@{/mfuc/banner/move.do}">
|
||||||
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||||
|
<input type="hidden" name="bannerIdntfNo" th:value="${banner.bannerIdntfNo}">
|
||||||
|
<input type="hidden" name="direction" value="down">
|
||||||
|
<button class="inline-flex w-full items-center justify-center rounded-lg border border-gray-200 px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
|
type="submit"
|
||||||
|
th:disabled="${stat.last}">
|
||||||
|
아래
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-4" style="width: 190px;">
|
||||||
|
<img class="h-28 w-40 rounded-xl object-cover"
|
||||||
|
th:src="${banner.imageUrl}"
|
||||||
|
th:alt="${banner.title}">
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-4">
|
||||||
|
<div class="font-semibold text-gray-900" th:text="${banner.title}">배너 제목</div>
|
||||||
|
<p class="iotdoor-banner-list-content mt-2 text-sm leading-6 text-gray-500" th:text="${banner.content}">배너 내용</p>
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-4">
|
||||||
|
<span class="inline-flex rounded-full px-3 py-1 text-xs font-medium"
|
||||||
|
th:classappend="${banner.useYn == 'Y'} ? ' bg-success-50 text-success-700' : ' bg-gray-100 text-gray-500'"
|
||||||
|
th:text="${banner.useYn == 'Y'} ? '노출' : '미노출'">노출</span>
|
||||||
|
</td>
|
||||||
|
<td class="whitespace-nowrap px-3 py-4 text-gray-500"
|
||||||
|
th:text="${#temporals.format(banner.registeredAt, 'yyyy-MM-dd HH:mm')}">2026-04-21 09:00</td>
|
||||||
|
<td class="px-3 py-4">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<a class="inline-flex items-center justify-center rounded-lg border border-brand-200 px-3 py-2 text-sm font-medium text-brand-600 hover:bg-brand-25"
|
||||||
|
th:href="@{/mfuc/banner/form.do(bannerIdntfNo=${banner.bannerIdntfNo})}">
|
||||||
|
수정
|
||||||
|
</a>
|
||||||
|
<form method="post" th:action="@{/mfuc/banner/delete.do}">
|
||||||
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||||
|
<input type="hidden" name="bannerIdntfNo" th:value="${banner.bannerIdntfNo}">
|
||||||
|
<button class="inline-flex w-full items-center justify-center rounded-lg border border-error-200 px-3 py-2 text-sm font-medium text-error-600 hover:bg-error-25"
|
||||||
|
type="submit"
|
||||||
|
onclick="return confirm('해당 배너를 삭제하시겠습니까?');">
|
||||||
|
삭제
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -67,6 +67,20 @@
|
|||||||
<span class="menu-item-text">포트폴리오 관리</span>
|
<span class="menu-item-text">포트폴리오 관리</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="menu-item group"
|
||||||
|
th:classappend="${adminMenu == 'banner'} ? ' menu-item-active' : ' menu-item-inactive'"
|
||||||
|
th:href="@{/mfuc/banner/list.do}">
|
||||||
|
<span class="menu-item-icon-size"
|
||||||
|
th:classappend="${adminMenu == 'banner'} ? ' menu-item-icon-active' : ' menu-item-icon-inactive'">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4.8 5.4h14.4A1.8 1.8 0 0 1 21 7.2v9.6a1.8 1.8 0 0 1-1.8 1.8H4.8A1.8 1.8 0 0 1 3 16.8V7.2a1.8 1.8 0 0 1 1.8-1.8Z" stroke="currentColor" stroke-width="1.5"/>
|
||||||
|
<path d="M7.2 9h9.6M7.2 12h6M7.2 15h3.6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="menu-item-text">배너관리</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="menu-item group"
|
<a class="menu-item group"
|
||||||
th:classappend="${adminMenu == 'contact'} ? ' menu-item-active' : ' menu-item-inactive'"
|
th:classappend="${adminMenu == 'contact'} ? ' menu-item-active' : ' menu-item-inactive'"
|
||||||
@@ -83,11 +97,30 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
<nav>
|
||||||
|
<h2 class="mb-4 text-xs uppercase leading-[20px] text-gray-400">통계</h2>
|
||||||
|
<ul class="flex flex-col gap-4">
|
||||||
|
<li>
|
||||||
|
<a class="menu-item group"
|
||||||
|
th:classappend="${adminMenu == 'statistics'} ? ' menu-item-active' : ' menu-item-inactive'"
|
||||||
|
th:href="@{/mfuc/statistics/access.do}">
|
||||||
|
<span class="menu-item-icon-size"
|
||||||
|
th:classappend="${adminMenu == 'statistics'} ? ' menu-item-icon-active' : ' menu-item-icon-inactive'">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4.8 19.2V12M12 19.2V4.8M19.2 19.2v-9.6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
<path d="M3.6 19.2h16.8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="menu-item-text">접속 통계</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div class="iotdoor-admin-main">
|
<div class="iotdoor-admin-main">
|
||||||
<header class="sticky top-0 z-999 bg-white/95 border-b border-gray-200 backdrop-blur-sm">
|
<header class="iotdoor-admin-header bg-white border-b border-gray-200">
|
||||||
<div class="flex flex-col gap-4 px-4 py-4 md:flex-row md:items-center md:justify-between md:px-6">
|
<div class="flex flex-col gap-4 px-4 py-4 md:flex-row md:items-center md:justify-between md:px-6">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-gray-500">운영 중인 관리자</p>
|
<p class="text-sm text-gray-500">운영 중인 관리자</p>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
data-current-thumbnail-file-sn=*{thumbnailFileSn}">
|
data-current-thumbnail-file-sn=*{thumbnailFileSn}">
|
||||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||||
<input type="hidden" th:field="*{portfolioIdntfNo}">
|
<input type="hidden" th:field="*{portfolioIdntfNo}">
|
||||||
|
<input type="hidden" th:field="*{registeredAt}">
|
||||||
<input type="hidden" th:field="*{thumbnailAtchFileIdntfNo}" id="thumbnailAtchFileIdntfNo">
|
<input type="hidden" th:field="*{thumbnailAtchFileIdntfNo}" id="thumbnailAtchFileIdntfNo">
|
||||||
<input type="hidden" th:field="*{thumbnailFileSn}" id="thumbnailFileSn">
|
<input type="hidden" th:field="*{thumbnailFileSn}" id="thumbnailFileSn">
|
||||||
<textarea hidden th:field="*{content}" id="content"></textarea>
|
<textarea hidden th:field="*{content}" id="content"></textarea>
|
||||||
|
|||||||
@@ -33,7 +33,15 @@
|
|||||||
아직 등록된 포트폴리오가 없습니다.
|
아직 등록된 포트폴리오가 없습니다.
|
||||||
</div>
|
</div>
|
||||||
<div th:unless="${#lists.isEmpty(portfolioItems)}" class="overflow-x-auto">
|
<div th:unless="${#lists.isEmpty(portfolioItems)}" class="overflow-x-auto">
|
||||||
<table class="min-w-full text-sm">
|
<table class="iotdoor-portfolio-table text-sm">
|
||||||
|
<colgroup>
|
||||||
|
<col class="portfolio-col-image">
|
||||||
|
<col class="portfolio-col-title">
|
||||||
|
<col class="portfolio-col-date">
|
||||||
|
<col class="portfolio-col-author">
|
||||||
|
<col class="portfolio-col-view">
|
||||||
|
<col class="portfolio-col-action">
|
||||||
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="border-b border-gray-100 text-left text-gray-500">
|
<tr class="border-b border-gray-100 text-left text-gray-500">
|
||||||
<th class="px-3 py-3">대표이미지</th>
|
<th class="px-3 py-3">대표이미지</th>
|
||||||
@@ -47,7 +55,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr class="border-b border-gray-100 align-top last:border-0"
|
<tr class="border-b border-gray-100 align-top last:border-0"
|
||||||
th:each="item : ${portfolioItems}">
|
th:each="item : ${portfolioItems}">
|
||||||
<td class="px-3 py-4" style="width: 160px;">
|
<td class="px-3 py-4">
|
||||||
<img class="h-24 w-36 rounded-xl object-cover"
|
<img class="h-24 w-36 rounded-xl object-cover"
|
||||||
th:src="${item.imageUrl}"
|
th:src="${item.imageUrl}"
|
||||||
th:alt="${item.title}">
|
th:alt="${item.title}">
|
||||||
@@ -61,12 +69,12 @@
|
|||||||
공개 페이지 보기
|
공개 페이지 보기
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-4 text-gray-500"
|
<td class="whitespace-nowrap px-3 py-4 text-gray-500"
|
||||||
th:text="${#temporals.format(item.registeredAt, 'yyyy-MM-dd HH:mm')}">2026-04-17 09:00</td>
|
th:text="${#temporals.format(item.registeredAt, 'yyyy-MM-dd HH:mm')}">2026-04-17 09:00</td>
|
||||||
<td class="px-3 py-4 text-gray-500" th:text="${item.registeredByName}">관리자</td>
|
<td class="whitespace-nowrap px-3 py-4 text-gray-500" th:text="${item.registeredByName}">관리자</td>
|
||||||
<td class="px-3 py-4 text-gray-500" th:text="${item.viewCount}">0</td>
|
<td class="whitespace-nowrap px-3 py-4 text-gray-500" th:text="${item.viewCount}">0</td>
|
||||||
<td class="px-3 py-4">
|
<td class="px-3 py-4">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="iotdoor-portfolio-actions">
|
||||||
<a class="inline-flex items-center justify-center rounded-lg border border-brand-200 px-3 py-2 text-sm font-medium text-brand-600 hover:bg-brand-25"
|
<a class="inline-flex items-center justify-center rounded-lg border border-brand-200 px-3 py-2 text-sm font-medium text-brand-600 hover:bg-brand-25"
|
||||||
th:href="@{/mfuc/portfolio/form.do(portfolioIdntfNo=${item.portfolioIdntfNo})}">
|
th:href="@{/mfuc/portfolio/form.do(portfolioIdntfNo=${item.portfolioIdntfNo})}">
|
||||||
수정
|
수정
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko"
|
||||||
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
|
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||||
|
layout:decorate="~{kr/iotdoor/boot/admin/layout/adminLayout}">
|
||||||
|
<body>
|
||||||
|
<div layout:fragment="content" class="space-y-6">
|
||||||
|
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||||
|
<div class="rounded-2xl border border-gray-200 bg-white p-5">
|
||||||
|
<p class="text-sm text-gray-500">전체 접속</p>
|
||||||
|
<h3 class="mt-3 text-3xl font-semibold text-gray-900" th:text="${summary.totalAccessCount}">0</h3>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-2xl border border-gray-200 bg-white p-5">
|
||||||
|
<p class="text-sm text-gray-500">오늘 접속</p>
|
||||||
|
<h3 class="mt-3 text-3xl font-semibold text-gray-900" th:text="${summary.todayAccessCount}">0</h3>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-2xl border border-gray-200 bg-white p-5">
|
||||||
|
<p class="text-sm text-gray-500">고유 IP</p>
|
||||||
|
<h3 class="mt-3 text-3xl font-semibold text-gray-900" th:text="${summary.uniqueIpCount}">0</h3>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-2xl border border-gray-200 bg-white p-5">
|
||||||
|
<p class="text-sm text-gray-500">고유 세션</p>
|
||||||
|
<h3 class="mt-3 text-3xl font-semibold text-gray-900" th:text="${summary.uniqueSessionCount}">0</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-6 xl:grid-cols-3">
|
||||||
|
<section class="rounded-2xl border border-gray-200 bg-white">
|
||||||
|
<div class="px-6 py-5">
|
||||||
|
<h2 class="text-base font-medium text-gray-800">일별 접속</h2>
|
||||||
|
</div>
|
||||||
|
<div class="border-t border-gray-100 p-4 sm:p-6">
|
||||||
|
<div th:if="${#lists.isEmpty(dailyStats)}" class="rounded-xl bg-gray-50 px-4 py-6 text-sm text-gray-500">
|
||||||
|
접속 기록이 없습니다.
|
||||||
|
</div>
|
||||||
|
<div th:unless="${#lists.isEmpty(dailyStats)}" class="overflow-x-auto">
|
||||||
|
<table class="min-w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-gray-100 text-left text-gray-500">
|
||||||
|
<th class="px-3 py-3">날짜</th>
|
||||||
|
<th class="px-3 py-3 text-right">접속수</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="border-b border-gray-100 last:border-0"
|
||||||
|
th:each="stat : ${dailyStats}">
|
||||||
|
<td class="px-3 py-4 text-gray-600"
|
||||||
|
th:text="${#temporals.format(stat.accessDate, 'yyyy-MM-dd')}">2026-04-21</td>
|
||||||
|
<td class="px-3 py-4 text-right font-medium text-gray-900" th:text="${stat.accessCount}">0</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="rounded-2xl border border-gray-200 bg-white">
|
||||||
|
<div class="px-6 py-5">
|
||||||
|
<h2 class="text-base font-medium text-gray-800">상위 페이지</h2>
|
||||||
|
</div>
|
||||||
|
<div class="border-t border-gray-100 p-4 sm:p-6">
|
||||||
|
<div th:if="${#lists.isEmpty(topPages)}" class="rounded-xl bg-gray-50 px-4 py-6 text-sm text-gray-500">
|
||||||
|
접속 기록이 없습니다.
|
||||||
|
</div>
|
||||||
|
<div th:unless="${#lists.isEmpty(topPages)}" class="space-y-4">
|
||||||
|
<div class="flex items-start justify-between gap-4"
|
||||||
|
th:each="page : ${topPages}">
|
||||||
|
<span class="min-w-0 flex-1 break-all text-sm text-gray-600" th:text="${page.label}">/</span>
|
||||||
|
<span class="text-sm font-medium text-gray-900" th:text="${page.accessCount}">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="rounded-2xl border border-gray-200 bg-white">
|
||||||
|
<div class="px-6 py-5">
|
||||||
|
<h2 class="text-base font-medium text-gray-800">상위 유입</h2>
|
||||||
|
</div>
|
||||||
|
<div class="border-t border-gray-100 p-4 sm:p-6">
|
||||||
|
<div th:if="${#lists.isEmpty(topReferers)}" class="rounded-xl bg-gray-50 px-4 py-6 text-sm text-gray-500">
|
||||||
|
접속 기록이 없습니다.
|
||||||
|
</div>
|
||||||
|
<div th:unless="${#lists.isEmpty(topReferers)}" class="space-y-4">
|
||||||
|
<div class="flex items-start justify-between gap-4"
|
||||||
|
th:each="referer : ${topReferers}">
|
||||||
|
<span class="min-w-0 flex-1 break-all text-sm text-gray-600" th:text="${referer.label}">직접 접속</span>
|
||||||
|
<span class="text-sm font-medium text-gray-900" th:text="${referer.accessCount}">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="rounded-2xl border border-gray-200 bg-white">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3 px-6 py-5">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-base font-medium text-gray-800">최근 접속 기록</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">최근 100건 기준</p>
|
||||||
|
</div>
|
||||||
|
<a class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
|
||||||
|
th:href="@{/mfuc/index.do}">
|
||||||
|
대시보드
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="border-t border-gray-100 p-4 sm:p-6">
|
||||||
|
<div th:if="${#lists.isEmpty(recentLogs)}" class="rounded-xl bg-gray-50 px-4 py-6 text-sm text-gray-500">
|
||||||
|
접속 기록이 없습니다.
|
||||||
|
</div>
|
||||||
|
<div th:unless="${#lists.isEmpty(recentLogs)}" class="overflow-x-auto">
|
||||||
|
<table class="iotdoor-access-log-table text-sm">
|
||||||
|
<colgroup>
|
||||||
|
<col class="access-log-col-time">
|
||||||
|
<col class="access-log-col-ip">
|
||||||
|
<col class="access-log-col-request">
|
||||||
|
<col class="access-log-col-referer">
|
||||||
|
<col class="access-log-col-session">
|
||||||
|
<col class="access-log-col-status">
|
||||||
|
<col class="access-log-col-agent">
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-gray-100 text-left text-gray-500">
|
||||||
|
<th class="px-3 py-3">등록시점</th>
|
||||||
|
<th class="px-3 py-3">IP</th>
|
||||||
|
<th class="px-3 py-3">요청</th>
|
||||||
|
<th class="px-3 py-3">Referer</th>
|
||||||
|
<th class="px-3 py-3">Session ID</th>
|
||||||
|
<th class="px-3 py-3">상태</th>
|
||||||
|
<th class="px-3 py-3">User-Agent</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="border-b border-gray-100 align-top last:border-0"
|
||||||
|
th:each="log : ${recentLogs}">
|
||||||
|
<td class="whitespace-nowrap px-3 py-4 text-gray-500"
|
||||||
|
th:text="${#temporals.format(log.registeredAt, 'yyyy-MM-dd HH:mm:ss')}">2026-04-21 10:00:00</td>
|
||||||
|
<td class="px-3 py-4 font-medium text-gray-900">
|
||||||
|
<span class="iotdoor-access-log-text iotdoor-access-log-mono" th:text="${log.accessIp}">127.0.0.1</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-4 text-gray-600">
|
||||||
|
<div class="font-medium text-gray-900" th:text="${log.httpMethod}">GET</div>
|
||||||
|
<div class="iotdoor-access-log-text mt-1" th:title="${log.fullRequestPath}" th:text="${log.fullRequestPath}">/</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-4 text-gray-500">
|
||||||
|
<span class="iotdoor-access-log-text"
|
||||||
|
th:title="${#strings.isEmpty(log.refererUrl) ? '-' : log.refererUrl}"
|
||||||
|
th:text="${#strings.isEmpty(log.refererUrl) ? '-' : log.refererUrl}">-</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-4 text-gray-500">
|
||||||
|
<span class="iotdoor-access-log-text iotdoor-access-log-mono"
|
||||||
|
th:title="${#strings.isEmpty(log.sessionId) ? '-' : log.sessionId}"
|
||||||
|
th:text="${#strings.isEmpty(log.sessionId) ? '-' : log.sessionId}">session</span>
|
||||||
|
</td>
|
||||||
|
<td class="whitespace-nowrap px-3 py-4 text-gray-500" th:text="${log.statusCode}">200</td>
|
||||||
|
<td class="px-3 py-4 text-gray-500">
|
||||||
|
<span class="iotdoor-access-log-text"
|
||||||
|
th:title="${#strings.isEmpty(log.userAgent) ? '-' : log.userAgent}"
|
||||||
|
th:text="${#strings.isEmpty(log.userAgent) ? '-' : log.userAgent}">Mozilla/5.0</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -5,13 +5,54 @@
|
|||||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||||
layout:decorate="~{kr/iotdoor/comn/site/layout/cmmnSiteLayout.html}">
|
layout:decorate="~{kr/iotdoor/comn/site/layout/cmmnSiteLayout.html}">
|
||||||
|
|
||||||
|
<th:block layout:fragment="pageStyles">
|
||||||
|
<link rel="stylesheet" th:href="@{/resources/kr/iotdoor/cmty/portfolio/portfolio-public.css?v=20260421}">
|
||||||
|
<link rel="stylesheet" th:href="@{/resources/kr/iotdoor/cmty/portfolio/portfolio-post-list.css?v=20260421}">
|
||||||
|
</th:block>
|
||||||
|
|
||||||
<th:block layout:fragment="content">
|
<th:block layout:fragment="content">
|
||||||
<div class="container py-5">
|
<div class="container py-5">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-lg-10">
|
<div class="col-lg-10">
|
||||||
<div class="mb-4">
|
<section class="portfolio-post-list-panel mb-4"
|
||||||
<a class="btn btn-sm btn-outline-secondary rounded-pill" th:href="@{/portfolio.do}">목록으로</a>
|
data-portfolio-list-panel
|
||||||
</div>
|
th:data-current-id="${portfolio.portfolioIdntfNo}"
|
||||||
|
th:data-list-url="@{/portfolio/items.json}">
|
||||||
|
<div class="portfolio-post-list-bar">
|
||||||
|
<div class="portfolio-post-list-summary">
|
||||||
|
<a th:href="@{/portfolio.do}">전체보기</a>
|
||||||
|
<span th:text="|${portfolioCount}개의 글|">0개의 글</span>
|
||||||
|
</div>
|
||||||
|
<button class="portfolio-post-list-toggle"
|
||||||
|
type="button"
|
||||||
|
aria-expanded="false"
|
||||||
|
data-portfolio-list-toggle>목록열기</button>
|
||||||
|
</div>
|
||||||
|
<div class="portfolio-post-list-body" hidden data-portfolio-list-body>
|
||||||
|
<div class="portfolio-post-list-grid ag-theme-quartz" data-portfolio-list-grid></div>
|
||||||
|
<div class="portfolio-post-list-footer">
|
||||||
|
<div class="portfolio-post-list-footer-side">
|
||||||
|
<a class="portfolio-post-manage-link"
|
||||||
|
sec:authorize="hasRole('ADMIN')"
|
||||||
|
th:href="@{/mfuc/portfolio/list.do}">글 관리 열기</a>
|
||||||
|
</div>
|
||||||
|
<nav class="portfolio-post-pagination"
|
||||||
|
aria-label="포트폴리오 목록 페이지"
|
||||||
|
data-portfolio-pagination></nav>
|
||||||
|
<div class="portfolio-post-list-footer-side portfolio-post-page-size-wrap">
|
||||||
|
<label class="visually-hidden" for="portfolioPageSize">페이지당 글 수</label>
|
||||||
|
<select id="portfolioPageSize"
|
||||||
|
class="portfolio-post-page-size"
|
||||||
|
data-portfolio-page-size>
|
||||||
|
<option value="5">5줄 보기</option>
|
||||||
|
<option value="10">10줄 보기</option>
|
||||||
|
<option value="20">20줄 보기</option>
|
||||||
|
<option value="50">50줄 보기</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<article class="bg-white rounded shadow-sm overflow-hidden">
|
<article class="bg-white rounded shadow-sm overflow-hidden">
|
||||||
<!-- <img class="img-fluid w-100" th:src="${portfolio.imageUrl}" th:alt="${portfolio.title}"> -->
|
<!-- <img class="img-fluid w-100" th:src="${portfolio.imageUrl}" th:alt="${portfolio.title}"> -->
|
||||||
@@ -29,4 +70,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
|
<th:block layout:fragment="script">
|
||||||
|
<script th:src="@{/resources/kr/iotdoor/cmty/portfolio/portfolio-public.js?v=20260421}"></script>
|
||||||
|
</th:block>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -5,6 +5,11 @@
|
|||||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||||
layout:decorate="~{kr/iotdoor/comn/site/layout/cmmnSiteLayout.html}">
|
layout:decorate="~{kr/iotdoor/comn/site/layout/cmmnSiteLayout.html}">
|
||||||
|
|
||||||
|
<th:block layout:fragment="pageStyles">
|
||||||
|
<link rel="stylesheet" th:href="@{/resources/kr/iotdoor/cmty/portfolio/portfolio-public.css?v=20260421}">
|
||||||
|
<link rel="stylesheet" th:href="@{/resources/kr/iotdoor/cmty/portfolio/portfolio-post-list.css?v=20260421}">
|
||||||
|
</th:block>
|
||||||
|
|
||||||
<th:block layout:fragment="content">
|
<th:block layout:fragment="content">
|
||||||
<div class="container-xxl py-5">
|
<div class="container-xxl py-5">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -14,6 +19,45 @@
|
|||||||
<p class="mb-0">대표 이미지, 상세 내용, 등록일시, 조회수까지 포함한 포트폴리오를 관리자 화면에서 직접 관리할 수 있습니다.</p>
|
<p class="mb-0">대표 이미지, 상세 내용, 등록일시, 조회수까지 포함한 포트폴리오를 관리자 화면에서 직접 관리할 수 있습니다.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section class="portfolio-post-list-panel mb-5"
|
||||||
|
data-portfolio-list-panel
|
||||||
|
th:data-list-url="@{/portfolio/items.json}">
|
||||||
|
<div class="portfolio-post-list-bar">
|
||||||
|
<div class="portfolio-post-list-summary">
|
||||||
|
<a th:href="@{/portfolio.do}">전체보기</a>
|
||||||
|
<span th:text="|${#lists.size(portfolioItems)}개의 글|">0개의 글</span>
|
||||||
|
</div>
|
||||||
|
<button class="portfolio-post-list-toggle"
|
||||||
|
type="button"
|
||||||
|
aria-expanded="false"
|
||||||
|
data-portfolio-list-toggle>목록열기</button>
|
||||||
|
</div>
|
||||||
|
<div class="portfolio-post-list-body" hidden data-portfolio-list-body>
|
||||||
|
<div class="portfolio-post-list-grid ag-theme-quartz" data-portfolio-list-grid></div>
|
||||||
|
<div class="portfolio-post-list-footer">
|
||||||
|
<div class="portfolio-post-list-footer-side">
|
||||||
|
<a class="portfolio-post-manage-link"
|
||||||
|
sec:authorize="hasRole('ADMIN')"
|
||||||
|
th:href="@{/mfuc/portfolio/list.do}">글 관리 열기</a>
|
||||||
|
</div>
|
||||||
|
<nav class="portfolio-post-pagination"
|
||||||
|
aria-label="포트폴리오 목록 페이지"
|
||||||
|
data-portfolio-pagination></nav>
|
||||||
|
<div class="portfolio-post-list-footer-side portfolio-post-page-size-wrap">
|
||||||
|
<label class="visually-hidden" for="portfolioPageSize">페이지당 글 수</label>
|
||||||
|
<select id="portfolioPageSize"
|
||||||
|
class="portfolio-post-page-size"
|
||||||
|
data-portfolio-page-size>
|
||||||
|
<option value="5">5줄 보기</option>
|
||||||
|
<option value="10">10줄 보기</option>
|
||||||
|
<option value="20">20줄 보기</option>
|
||||||
|
<option value="50">50줄 보기</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div th:if="${#lists.isEmpty(portfolioItems)}" class="text-center text-secondary py-5">
|
<div th:if="${#lists.isEmpty(portfolioItems)}" class="text-center text-secondary py-5">
|
||||||
아직 등록된 포트폴리오가 없습니다.
|
아직 등록된 포트폴리오가 없습니다.
|
||||||
</div>
|
</div>
|
||||||
@@ -22,25 +66,20 @@
|
|||||||
<div class="col-md-6 col-lg-4 wow fadeInUp"
|
<div class="col-md-6 col-lg-4 wow fadeInUp"
|
||||||
data-wow-delay="0.1s"
|
data-wow-delay="0.1s"
|
||||||
th:each="item : ${portfolioItems}">
|
th:each="item : ${portfolioItems}">
|
||||||
<div class="service-item rounded overflow-hidden h-100 align-content-center">
|
<a class="portfolio-frame-card"
|
||||||
<a th:href="@{/portfolio/{portfolioId}(portfolioId=${item.portfolioIdntfNo})}">
|
th:href="@{/portfolio/{portfolioId}(portfolioId=${item.portfolioIdntfNo})}">
|
||||||
<img class="img-fluid" th:src="${item.imageUrl}" th:alt="${item.title}">
|
<span class="portfolio-frame-image-link">
|
||||||
</a>
|
<img class="portfolio-frame-image" th:src="${item.imageUrl}" th:alt="${item.title}">
|
||||||
<div class="position-relative p-4 pt-0">
|
</span>
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2 small text-secondary">
|
<div class="portfolio-frame-body">
|
||||||
|
<div class="portfolio-frame-meta">
|
||||||
<span th:text="${#temporals.format(item.registeredAt, 'yyyy-MM-dd')}">2026-04-16</span>
|
<span th:text="${#temporals.format(item.registeredAt, 'yyyy-MM-dd')}">2026-04-16</span>
|
||||||
<span>조회 <span th:text="${item.viewCount}">0</span></span>
|
<span>조회 <span th:text="${item.viewCount}">0</span></span>
|
||||||
</div>
|
</div>
|
||||||
<h4 class="mb-3">
|
<h4 class="portfolio-frame-title" th:text="${item.title}">제목</h4>
|
||||||
<a class="text-dark" th:href="@{/portfolio/{portfolioId}(portfolioId=${item.portfolioIdntfNo})}" th:text="${item.title}">제목</a>
|
<p class="portfolio-frame-summary" th:text="${item.summary}">설명</p>
|
||||||
</h4>
|
|
||||||
<p class="mb-3" th:text="${item.summary}">설명</p>
|
|
||||||
<a class="btn btn-sm btn-outline-primary rounded-pill"
|
|
||||||
th:href="@{/portfolio/{portfolioId}(portfolioId=${item.portfolioIdntfNo})}">
|
|
||||||
자세히 보기
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -50,4 +89,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
|
<th:block layout:fragment="script">
|
||||||
|
<script th:src="@{/resources/kr/iotdoor/cmty/portfolio/portfolio-public.js?v=20260421}"></script>
|
||||||
|
</th:block>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<!-- Spinner Start -->
|
<!-- Spinner Start -->
|
||||||
<div id="spinner" class="show bg-white position-fixed translate-middle w-100 vh-100 top-50 start-50 d-flex align-items-center justify-content-center">
|
<div id="spinner"
|
||||||
|
class="show bg-white position-fixed translate-middle w-100 vh-100 top-50 start-50 d-flex align-items-center justify-content-center">
|
||||||
<div class="spinner-border text-primary" style="width: 3rem; height: 3rem;" role="status">
|
<div class="spinner-border text-primary" style="width: 3rem; height: 3rem;" role="status">
|
||||||
<span class="sr-only">Loading...</span>
|
<span class="sr-only">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,9 +31,12 @@
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-100 d-inline-flex align-items-center mx-n2">
|
<div class="h-100 d-inline-flex align-items-center mx-n2">
|
||||||
<a class="btn btn-square btn-link rounded-0 border-0 border-end border-secondary" href=""><i class="fab fa-facebook-f"></i></a>
|
<a class="btn btn-square btn-link rounded-0 border-0 border-end border-secondary" href=""><i
|
||||||
<a class="btn btn-square btn-link rounded-0 border-0 border-end border-secondary" href=""><i class="fab fa-twitter"></i></a>
|
class="fab fa-facebook-f"></i></a>
|
||||||
<a class="btn btn-square btn-link rounded-0 border-0 border-end border-secondary" href=""><i class="fab fa-linkedin-in"></i></a>
|
<a class="btn btn-square btn-link rounded-0 border-0 border-end border-secondary" href=""><i
|
||||||
|
class="fab fa-twitter"></i></a>
|
||||||
|
<a class="btn btn-square btn-link rounded-0 border-0 border-end border-secondary" href=""><i
|
||||||
|
class="fab fa-linkedin-in"></i></a>
|
||||||
<a class="btn btn-square btn-link rounded-0" href=""><i class="fab fa-instagram"></i></a>
|
<a class="btn btn-square btn-link rounded-0" href=""><i class="fab fa-instagram"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,13 +51,14 @@
|
|||||||
<!-- <h2 class="m-0 text-primary">국제오토텍</h2> -->
|
<!-- <h2 class="m-0 text-primary">국제오토텍</h2> -->
|
||||||
<img src="/resources/kr/iotdoor/comn/site/layout/image/template/logo.jpg" alt="로고" style="height:75px;">
|
<img src="/resources/kr/iotdoor/comn/site/layout/image/template/logo.jpg" alt="로고" style="height:75px;">
|
||||||
</a>
|
</a>
|
||||||
<button type="button" class="navbar-toggler me-4" data-bs-toggle="collapse" data-bs-target="#navbarCollapse">
|
<button type="button" class="navbar-toggler me-4" data-bs-toggle="collapse" data-bs-target="#navbarCollapse"
|
||||||
|
aria-controls="navbarCollapse" aria-expanded="false" aria-label="메뉴 열기">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||||
<div class="navbar-nav ms-auto p-4 p-lg-0">
|
<div class="navbar-nav ms-auto p-4 p-lg-0">
|
||||||
<a href="/" class="nav-item nav-link">Home</a>
|
<a href="/" class="nav-item nav-link">Home</a>
|
||||||
<a href="/" class="nav-item nav-link">About</a>
|
<!-- <a href="/" class="nav-item nav-link">About</a> -->
|
||||||
<a href="/portfolio.do" class="nav-item nav-link">Portfolio</a>
|
<a href="/portfolio.do" class="nav-item nav-link">Portfolio</a>
|
||||||
<a href="/contact.do" class="nav-item nav-link">Contact</a>
|
<a href="/contact.do" class="nav-item nav-link">Contact</a>
|
||||||
<a href="/mfuc/index.do" class="nav-item nav-link">ADMIN</a>
|
<a href="/mfuc/index.do" class="nav-item nav-link">ADMIN</a>
|
||||||
|
|||||||
@@ -38,6 +38,8 @@
|
|||||||
|
|
||||||
<!-- Template Stylesheet -->
|
<!-- Template Stylesheet -->
|
||||||
<link href="/resources/kr/iotdoor/comn/site/layout/css/templateStyle.css?220712" rel="stylesheet" />
|
<link href="/resources/kr/iotdoor/comn/site/layout/css/templateStyle.css?220712" rel="stylesheet" />
|
||||||
|
<link rel="stylesheet" th:href="@{/resources/kr/iotdoor/comn/site/layout/css/custom.css?v=20260421}">
|
||||||
|
<th:block layout:fragment="pageStyles"></th:block>
|
||||||
|
|
||||||
<!-- <link th:if="${mainYn == 'Y' && popupList.size > 0}" rel="stylesheet" th:href="@{/resources/kr/iotdoor/comn/popup/css/popupListLayout.css}"> -->
|
<!-- <link th:if="${mainYn == 'Y' && popupList.size > 0}" rel="stylesheet" th:href="@{/resources/kr/iotdoor/comn/popup/css/popupListLayout.css}"> -->
|
||||||
<!-- <script th:if="${not #strings.isEmpty(checkMessage)}">alert('[[${checkMessage}]]');</script> -->
|
<!-- <script th:if="${not #strings.isEmpty(checkMessage)}">alert('[[${checkMessage}]]');</script> -->
|
||||||
@@ -101,7 +103,7 @@
|
|||||||
<script th:src="@{'//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js'}"></script>
|
<script th:src="@{'//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js'}"></script>
|
||||||
<!-- Includes all JS & CSS for the JavaScript Data Grid -->
|
<!-- Includes all JS & CSS for the JavaScript Data Grid -->
|
||||||
<script th:src="@{/resources/kr/iotdoor/comn/site/layout/js/common.js?v=20240716}"></script>
|
<script th:src="@{/resources/kr/iotdoor/comn/site/layout/js/common.js?v=20240716}"></script>
|
||||||
<script th:src="@{/resources/kr/iotdoor/comn/site/layout/js/main.js?v=20240716}"></script>
|
<script th:src="@{/resources/kr/iotdoor/comn/site/layout/js/main.js?v=20260507}"></script>
|
||||||
<script th:if="${mainYn == 'Y' and !#lists.isEmpty(popupList)}" th:src="@{/resources/kr/iotdoor/comn/popup/js/popupListLayout.js}"></script>
|
<script th:if="${mainYn == 'Y' and !#lists.isEmpty(popupList)}" th:src="@{/resources/kr/iotdoor/comn/popup/js/popupListLayout.js}"></script>
|
||||||
<th:block layout:fragment="script"></th:block>
|
<th:block layout:fragment="script"></th:block>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -83,139 +83,27 @@
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<div
|
<div th:if="${#lists.isEmpty(mainBanners)}" class="col-12">
|
||||||
class="col-md-6 col-lg-4 wow fadeInUp"
|
<div class="rounded bg-light p-4 text-center text-muted">
|
||||||
data-wow-delay="0.1s"
|
등록된 배너가 없습니다.
|
||||||
>
|
|
||||||
<div class="service-item rounded overflow-hidden">
|
|
||||||
<img
|
|
||||||
class="img-fluid"
|
|
||||||
src="/resources/kr/iotdoor/comn/site/layout/image/template/img-600x400-1.jpg"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<div class="position-relative p-4 pt-0">
|
|
||||||
<div class="service-icon">
|
|
||||||
<i class="fa fa-solar-panel fa-3x"></i>
|
|
||||||
</div>
|
|
||||||
<h4 class="mb-3">슬라이딩자동문</h4>
|
|
||||||
<!-- <p>슬라이드 오토도어를 전문적으로 다룹니다.</p> -->
|
|
||||||
<a class="small fw-medium" th:href="@{/portfolio.do}"
|
|
||||||
>Read More<i class="fa fa-arrow-right ms-2"></i></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="col-md-6 col-lg-4 wow fadeInUp"
|
||||||
class="col-md-6 col-lg-4 wow fadeInUp"
|
th:each="banner, bannerStat : ${mainBanners}"
|
||||||
data-wow-delay="0.3s"
|
th:attr="data-wow-delay=${bannerStat.index % 3 == 0 ? '0.1s' : (bannerStat.index % 3 == 1 ? '0.3s' : '0.5s')}">
|
||||||
>
|
<div class="service-item main-service-card rounded overflow-hidden">
|
||||||
<div class="service-item rounded overflow-hidden">
|
<img class="img-fluid main-service-card-image"
|
||||||
<img
|
th:src="${banner.imageUrl}"
|
||||||
class="img-fluid"
|
th:alt="${banner.title}"/>
|
||||||
src="/resources/kr/iotdoor/comn/site/layout/image/template/img-600x400-2.jpg"
|
<div class="main-service-card-body position-relative p-4 pt-0">
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<div class="position-relative p-4 pt-0">
|
|
||||||
<div class="service-icon">
|
<div class="service-icon">
|
||||||
<i class="fa fa-wind fa-3x"></i>
|
<i th:class="${bannerStat.index % 3 == 0 ? 'fa fa-solar-panel fa-3x' : (bannerStat.index % 3 == 1 ? 'fa fa-wind fa-3x' : 'fa fa-lightbulb fa-3x')}"></i>
|
||||||
</div>
|
</div>
|
||||||
<h4 class="mb-3">산업용자동문</h4>
|
<h4 class="mb-3" th:text="${banner.title}">배너 제목</h4>
|
||||||
<a class="small fw-medium" th:href="@{/portfolio.do}"
|
<p class="main-service-card-content" th:if="${!#strings.isEmpty(banner.content)}" th:text="${banner.content}">배너 내용</p>
|
||||||
>Read More<i
|
<a class="main-service-card-link small fw-medium" th:href="@{/portfolio.do}">
|
||||||
class="fa fa-arrow-right ms-2"
|
Read More<i class="fa fa-arrow-right ms-2"></i>
|
||||||
></i
|
</a>
|
||||||
></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="col-md-6 col-lg-4 wow fadeInUp"
|
|
||||||
data-wow-delay="0.5s"
|
|
||||||
>
|
|
||||||
<div class="service-item rounded overflow-hidden">
|
|
||||||
<img
|
|
||||||
class="img-fluid"
|
|
||||||
src="/resources/kr/iotdoor/comn/site/layout/image/template/img-600x400-3.jpg"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<div class="position-relative p-4 pt-0">
|
|
||||||
<div class="service-icon">
|
|
||||||
<i class="fa fa-lightbulb fa-3x"></i>
|
|
||||||
</div>
|
|
||||||
<h4 class="mb-3">고급산업용자동문</h4>
|
|
||||||
<a class="small fw-medium" th:href="@{/portfolio.do}"
|
|
||||||
>Read More<i
|
|
||||||
class="fa fa-arrow-right ms-2"
|
|
||||||
></i
|
|
||||||
></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="col-md-6 col-lg-4 wow fadeInUp"
|
|
||||||
data-wow-delay="0.1s"
|
|
||||||
>
|
|
||||||
<div class="service-item rounded overflow-hidden">
|
|
||||||
<img
|
|
||||||
class="img-fluid"
|
|
||||||
src="/resources/kr/iotdoor/comn/site/layout/image/template/img-600x400-4.jpg"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<div class="position-relative p-4 pt-0">
|
|
||||||
<div class="service-icon">
|
|
||||||
<i class="fa fa-solar-panel fa-3x"></i>
|
|
||||||
</div>
|
|
||||||
<h4 class="mb-3">내풍압고속셔터</h4>
|
|
||||||
<a class="small fw-medium" th:href="@{/portfolio.do}"
|
|
||||||
>Read More<i
|
|
||||||
class="fa fa-arrow-right ms-2"
|
|
||||||
></i
|
|
||||||
></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="col-md-6 col-lg-4 wow fadeInUp"
|
|
||||||
data-wow-delay="0.3s"
|
|
||||||
>
|
|
||||||
<div class="service-item rounded overflow-hidden">
|
|
||||||
<img
|
|
||||||
class="img-fluid"
|
|
||||||
src="/resources/kr/iotdoor/comn/site/layout/image/template/img-600x400-5.jpg?1"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<div class="position-relative p-4 pt-0">
|
|
||||||
<div class="service-icon">
|
|
||||||
<i class="fa fa-wind fa-3x"></i>
|
|
||||||
</div>
|
|
||||||
<h4 class="mb-3">오버헤드도어</h4>
|
|
||||||
<a class="small fw-medium" th:href="@{/portfolio.do}"
|
|
||||||
>Read More<i
|
|
||||||
class="fa fa-arrow-right ms-2"
|
|
||||||
></i
|
|
||||||
></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="col-md-6 col-lg-4 wow fadeInUp"
|
|
||||||
data-wow-delay="0.5s"
|
|
||||||
>
|
|
||||||
<div class="service-item rounded overflow-hidden">
|
|
||||||
<img
|
|
||||||
class="img-fluid"
|
|
||||||
src="/resources/kr/iotdoor/comn/site/layout/image/template/img-600x400-6.jpg?1?1"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<div class="position-relative p-4 pt-0">
|
|
||||||
<div class="service-icon">
|
|
||||||
<i class="fa fa-lightbulb fa-3x"></i>
|
|
||||||
</div>
|
|
||||||
<h4 class="mb-3">방화용슬라이딩자동문</h4>
|
|
||||||
<a class="small fw-medium" th:href="@{/portfolio.do}"
|
|
||||||
>Read More<i
|
|
||||||
class="fa fa-arrow-right ms-2"
|
|
||||||
></i
|
|
||||||
></a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -340,17 +228,17 @@
|
|||||||
<h6 class="text-primary">Contact Us</h6>
|
<h6 class="text-primary">Contact Us</h6>
|
||||||
<p class="mb-4">내용을 자세하게 입력해주시면 상담진행시에 좀 더 원활하게 상담이 가능합니다. 연중무휴 24시간 주저말고 남겨주세요. </p>
|
<p class="mb-4">내용을 자세하게 입력해주시면 상담진행시에 좀 더 원활하게 상담이 가능합니다. 연중무휴 24시간 주저말고 남겨주세요. </p>
|
||||||
<form id="contactForm">
|
<form id="contactForm">
|
||||||
<input type="hidden" name="title" value="국제오토텍 문의 접수">
|
<input type="hidden" name="title">
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input type="text" class="form-control" id="name" name="name" placeholder="Your Name" value="김찬솔">
|
<input type="text" class="form-control" id="name" name="name" placeholder="Your Name">
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input type="email" class="form-control" id="email" name="email" placeholder="Your Email" value="so__l91@naver.com">
|
<input type="email" class="form-control" id="email" name="email" placeholder="Your Email">
|
||||||
<label for="email">Email</label>
|
<label for="email">Email</label>
|
||||||
</div>
|
</div>
|
||||||
<div id="emailError" style="color:red; font-size:13px; display:none;">
|
<div id="emailError" style="color:red; font-size:13px; display:none;">
|
||||||
@@ -359,7 +247,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input type="text" class="form-control" id="phone" name="phone" placeholder="Phone" value="010-7722-0709">
|
<input type="text" class="form-control" id="phone" name="phone" placeholder="Phone">
|
||||||
<label for="subject">Phone</label>
|
<label for="subject">Phone</label>
|
||||||
</div>
|
</div>
|
||||||
<div id="phoneError" style="color:red; font-size:13px; display:none;">
|
<div id="phoneError" style="color:red; font-size:13px; display:none;">
|
||||||
@@ -368,13 +256,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input type="text" class="form-control" id="subject" name="subject" placeholder="Subject" value="제목324">
|
<input type="text" class="form-control" id="subject" name="subject" placeholder="Subject">
|
||||||
<label for="subject">Subject</label>
|
<label for="subject">Subject</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<textarea class="form-control" placeholder="Leave a message here" id="message" name="message" style="height: 100px">내용324234</textarea>
|
<textarea class="form-control" placeholder="Leave a message here" id="message" name="message" style="height: 100px"></textarea>
|
||||||
<label for="message">Message</label>
|
<label for="message">Message</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 181 KiB |
@@ -0,0 +1,81 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<!-- [D] XHTML 1.0 형식에 맞게 언어속성 추가-->
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang=“ko“ lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Pragma" content="no-cache"/>
|
||||||
|
<meta http-equiv="Expires" content="-1"/>
|
||||||
|
<meta name="robots" content="noindex,follow"/>
|
||||||
|
<meta name="referrer" content="always"/>
|
||||||
|
<meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="https://ssl.pstatic.net/static/blog/icon/favicon.ico" />
|
||||||
|
<link rel="alternate" type="application/rss+xml" href="https://rss.blog.naver.com/iotdoor1749.xml" title="RSS feed for iotdoor1749 Blog"/>
|
||||||
|
<link rel="wlwmanifest" type="application/wlwmanifest+xml" href="https://blog.naver.com/NBlogWlwLayout.naver?blogId=iotdoor1749" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<title>국제오토텍자동문 : 네이버 블로그</title>
|
||||||
|
</head>
|
||||||
|
<script type="text/javascript" src="https://ssl.pstatic.net/t.static.blog/mylog/versioning/Frameset-341345692_https.js" charset="UTF-8"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript" charset="UTF-8">
|
||||||
|
var photoContent="";
|
||||||
|
var postContent="";
|
||||||
|
|
||||||
|
var videoId = "";
|
||||||
|
var thumbnail = "";
|
||||||
|
var inKey = "";
|
||||||
|
var movieFileSize = "";
|
||||||
|
var playTime = "";
|
||||||
|
var screenSize = "";
|
||||||
|
|
||||||
|
var blogId = 'iotdoor1749';
|
||||||
|
var blogURL = 'https://blog.naver.com';
|
||||||
|
var eventCnt = '';
|
||||||
|
|
||||||
|
var g_ShareObject = {};
|
||||||
|
g_ShareObject.referer = "https%3A%2F%2Fm.blog.naver.com%2FPostView.naver%3FblogId%3Diotdoor1749%26logNo%3D223436271007";
|
||||||
|
|
||||||
|
|
||||||
|
jsMVC.setController("framesetTitleController", FramesetTitleController);
|
||||||
|
jsMVC.setController("framesetUrlController", FramesetUrlController);
|
||||||
|
jsMVC.setController("framesetMusicController", FramesetMusicController);
|
||||||
|
var oFramesetTitleController = jsMVC.getController("framesetTitleController");
|
||||||
|
var oFramesetUrlController = jsMVC.getController("framesetUrlController");
|
||||||
|
var oFramesetMusicController = jsMVC.getController("framesetMusicController");
|
||||||
|
var sTitle = document.title;
|
||||||
|
|
||||||
|
var topFrameAlert = function(message){
|
||||||
|
alert(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
var topFrameConfirm = function(message){
|
||||||
|
if(confirm(message)){
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style type="text/css">
|
||||||
|
html{width:100%;height:100%;}
|
||||||
|
body{width:100%;height:100%;margin:0;padding:0;font-size:0;}
|
||||||
|
#mainFrame{width:100%;height:100%;margin:0;padding:0;border:0;}
|
||||||
|
#hiddenFrame{width:0;height:0;margin:0;padding:0;border:0;}
|
||||||
|
</style>
|
||||||
|
<body>
|
||||||
|
<iframe id="mainFrame" name="mainFrame" allowfullscreen="true"
|
||||||
|
src="/PostList.naver?blogId=iotdoor1749&widgetTypeCall=true&categoryNo=1&topReferer=https%3A%2F%2Fm.blog.naver.com%2FPostView.naver%3FblogId%3Diotdoor1749%26logNo%3D223436271007&trackingCode=blog_postview&directAccess=true"
|
||||||
|
scrolling="auto"
|
||||||
|
onload="oFramesetTitleController.start(self.frames['mainFrame'], self, sTitle);oFramesetTitleController.onLoadFrame();oFramesetUrlController.start(self.frames['mainFrame']);oFramesetUrlController.onLoadFrame();document.querySelector('#mainFrame').focus();"
|
||||||
|
allowfullscreen></iframe>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 599 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 326 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 322 KiB |
|
After Width: | Height: | Size: 67 KiB |
@@ -0,0 +1,81 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<!-- [D] XHTML 1.0 형식에 맞게 언어속성 추가-->
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang=“ko“ lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Pragma" content="no-cache"/>
|
||||||
|
<meta http-equiv="Expires" content="-1"/>
|
||||||
|
<meta name="robots" content="noindex,follow"/>
|
||||||
|
<meta name="referrer" content="always"/>
|
||||||
|
<meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="https://ssl.pstatic.net/static/blog/icon/favicon.ico" />
|
||||||
|
<link rel="alternate" type="application/rss+xml" href="https://rss.blog.naver.com/iotdoor1749.xml" title="RSS feed for iotdoor1749 Blog"/>
|
||||||
|
<link rel="wlwmanifest" type="application/wlwmanifest+xml" href="https://blog.naver.com/NBlogWlwLayout.naver?blogId=iotdoor1749" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<title>국제오토텍자동문 : 네이버 블로그</title>
|
||||||
|
</head>
|
||||||
|
<script type="text/javascript" src="https://ssl.pstatic.net/t.static.blog/mylog/versioning/Frameset-341345692_https.js" charset="UTF-8"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript" charset="UTF-8">
|
||||||
|
var photoContent="";
|
||||||
|
var postContent="";
|
||||||
|
|
||||||
|
var videoId = "";
|
||||||
|
var thumbnail = "";
|
||||||
|
var inKey = "";
|
||||||
|
var movieFileSize = "";
|
||||||
|
var playTime = "";
|
||||||
|
var screenSize = "";
|
||||||
|
|
||||||
|
var blogId = 'iotdoor1749';
|
||||||
|
var blogURL = 'https://blog.naver.com';
|
||||||
|
var eventCnt = '';
|
||||||
|
|
||||||
|
var g_ShareObject = {};
|
||||||
|
g_ShareObject.referer = "https%3A%2F%2Fm.blog.naver.com%2FPostView.naver%3FblogId%3Diotdoor1749%26logNo%3D223271799927";
|
||||||
|
|
||||||
|
|
||||||
|
jsMVC.setController("framesetTitleController", FramesetTitleController);
|
||||||
|
jsMVC.setController("framesetUrlController", FramesetUrlController);
|
||||||
|
jsMVC.setController("framesetMusicController", FramesetMusicController);
|
||||||
|
var oFramesetTitleController = jsMVC.getController("framesetTitleController");
|
||||||
|
var oFramesetUrlController = jsMVC.getController("framesetUrlController");
|
||||||
|
var oFramesetMusicController = jsMVC.getController("framesetMusicController");
|
||||||
|
var sTitle = document.title;
|
||||||
|
|
||||||
|
var topFrameAlert = function(message){
|
||||||
|
alert(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
var topFrameConfirm = function(message){
|
||||||
|
if(confirm(message)){
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style type="text/css">
|
||||||
|
html{width:100%;height:100%;}
|
||||||
|
body{width:100%;height:100%;margin:0;padding:0;font-size:0;}
|
||||||
|
#mainFrame{width:100%;height:100%;margin:0;padding:0;border:0;}
|
||||||
|
#hiddenFrame{width:0;height:0;margin:0;padding:0;border:0;}
|
||||||
|
</style>
|
||||||
|
<body>
|
||||||
|
<iframe id="mainFrame" name="mainFrame" allowfullscreen="true"
|
||||||
|
src="/PostList.naver?blogId=iotdoor1749&widgetTypeCall=true&categoryNo=1&topReferer=https%3A%2F%2Fm.blog.naver.com%2FPostView.naver%3FblogId%3Diotdoor1749%26logNo%3D223271799927&trackingCode=blog_postview&directAccess=true"
|
||||||
|
scrolling="auto"
|
||||||
|
onload="oFramesetTitleController.start(self.frames['mainFrame'], self, sTitle);oFramesetTitleController.onLoadFrame();oFramesetUrlController.start(self.frames['mainFrame']);oFramesetUrlController.onLoadFrame();document.querySelector('#mainFrame').focus();"
|
||||||
|
allowfullscreen></iframe>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 333 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 448 KiB |
@@ -0,0 +1,81 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<!-- [D] XHTML 1.0 형식에 맞게 언어속성 추가-->
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang=“ko“ lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Pragma" content="no-cache"/>
|
||||||
|
<meta http-equiv="Expires" content="-1"/>
|
||||||
|
<meta name="robots" content="noindex,follow"/>
|
||||||
|
<meta name="referrer" content="always"/>
|
||||||
|
<meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="https://ssl.pstatic.net/static/blog/icon/favicon.ico" />
|
||||||
|
<link rel="alternate" type="application/rss+xml" href="https://rss.blog.naver.com/iotdoor1749.xml" title="RSS feed for iotdoor1749 Blog"/>
|
||||||
|
<link rel="wlwmanifest" type="application/wlwmanifest+xml" href="https://blog.naver.com/NBlogWlwLayout.naver?blogId=iotdoor1749" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<title>국제오토텍자동문 : 네이버 블로그</title>
|
||||||
|
</head>
|
||||||
|
<script type="text/javascript" src="https://ssl.pstatic.net/t.static.blog/mylog/versioning/Frameset-341345692_https.js" charset="UTF-8"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript" charset="UTF-8">
|
||||||
|
var photoContent="";
|
||||||
|
var postContent="";
|
||||||
|
|
||||||
|
var videoId = "";
|
||||||
|
var thumbnail = "";
|
||||||
|
var inKey = "";
|
||||||
|
var movieFileSize = "";
|
||||||
|
var playTime = "";
|
||||||
|
var screenSize = "";
|
||||||
|
|
||||||
|
var blogId = 'iotdoor1749';
|
||||||
|
var blogURL = 'https://blog.naver.com';
|
||||||
|
var eventCnt = '';
|
||||||
|
|
||||||
|
var g_ShareObject = {};
|
||||||
|
g_ShareObject.referer = "https%3A%2F%2Fm.blog.naver.com%2FPostView.naver%3FblogId%3Diotdoor1749%26logNo%3D223646371172";
|
||||||
|
|
||||||
|
|
||||||
|
jsMVC.setController("framesetTitleController", FramesetTitleController);
|
||||||
|
jsMVC.setController("framesetUrlController", FramesetUrlController);
|
||||||
|
jsMVC.setController("framesetMusicController", FramesetMusicController);
|
||||||
|
var oFramesetTitleController = jsMVC.getController("framesetTitleController");
|
||||||
|
var oFramesetUrlController = jsMVC.getController("framesetUrlController");
|
||||||
|
var oFramesetMusicController = jsMVC.getController("framesetMusicController");
|
||||||
|
var sTitle = document.title;
|
||||||
|
|
||||||
|
var topFrameAlert = function(message){
|
||||||
|
alert(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
var topFrameConfirm = function(message){
|
||||||
|
if(confirm(message)){
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style type="text/css">
|
||||||
|
html{width:100%;height:100%;}
|
||||||
|
body{width:100%;height:100%;margin:0;padding:0;font-size:0;}
|
||||||
|
#mainFrame{width:100%;height:100%;margin:0;padding:0;border:0;}
|
||||||
|
#hiddenFrame{width:0;height:0;margin:0;padding:0;border:0;}
|
||||||
|
</style>
|
||||||
|
<body>
|
||||||
|
<iframe id="mainFrame" name="mainFrame" allowfullscreen="true"
|
||||||
|
src="/PostList.naver?blogId=iotdoor1749&widgetTypeCall=true&categoryNo=1&topReferer=https%3A%2F%2Fm.blog.naver.com%2FPostView.naver%3FblogId%3Diotdoor1749%26logNo%3D223646371172&trackingCode=blog_postview&directAccess=true"
|
||||||
|
scrolling="auto"
|
||||||
|
onload="oFramesetTitleController.start(self.frames['mainFrame'], self, sTitle);oFramesetTitleController.onLoadFrame();oFramesetUrlController.start(self.frames['mainFrame']);oFramesetUrlController.onLoadFrame();document.querySelector('#mainFrame').focus();"
|
||||||
|
allowfullscreen></iframe>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 177 KiB |
|
After Width: | Height: | Size: 247 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 359 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 228 KiB |
|
After Width: | Height: | Size: 219 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,81 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<!-- [D] XHTML 1.0 형식에 맞게 언어속성 추가-->
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang=“ko“ lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Pragma" content="no-cache"/>
|
||||||
|
<meta http-equiv="Expires" content="-1"/>
|
||||||
|
<meta name="robots" content="noindex,follow"/>
|
||||||
|
<meta name="referrer" content="always"/>
|
||||||
|
<meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="https://ssl.pstatic.net/static/blog/icon/favicon.ico" />
|
||||||
|
<link rel="alternate" type="application/rss+xml" href="https://rss.blog.naver.com/iotdoor1749.xml" title="RSS feed for iotdoor1749 Blog"/>
|
||||||
|
<link rel="wlwmanifest" type="application/wlwmanifest+xml" href="https://blog.naver.com/NBlogWlwLayout.naver?blogId=iotdoor1749" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<title>국제오토텍자동문 : 네이버 블로그</title>
|
||||||
|
</head>
|
||||||
|
<script type="text/javascript" src="https://ssl.pstatic.net/t.static.blog/mylog/versioning/Frameset-341345692_https.js" charset="UTF-8"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript" charset="UTF-8">
|
||||||
|
var photoContent="";
|
||||||
|
var postContent="";
|
||||||
|
|
||||||
|
var videoId = "";
|
||||||
|
var thumbnail = "";
|
||||||
|
var inKey = "";
|
||||||
|
var movieFileSize = "";
|
||||||
|
var playTime = "";
|
||||||
|
var screenSize = "";
|
||||||
|
|
||||||
|
var blogId = 'iotdoor1749';
|
||||||
|
var blogURL = 'https://blog.naver.com';
|
||||||
|
var eventCnt = '';
|
||||||
|
|
||||||
|
var g_ShareObject = {};
|
||||||
|
g_ShareObject.referer = "https%3A%2F%2Fm.blog.naver.com%2FPostView.naver%3FblogId%3Diotdoor1749%26logNo%3D223606707258";
|
||||||
|
|
||||||
|
|
||||||
|
jsMVC.setController("framesetTitleController", FramesetTitleController);
|
||||||
|
jsMVC.setController("framesetUrlController", FramesetUrlController);
|
||||||
|
jsMVC.setController("framesetMusicController", FramesetMusicController);
|
||||||
|
var oFramesetTitleController = jsMVC.getController("framesetTitleController");
|
||||||
|
var oFramesetUrlController = jsMVC.getController("framesetUrlController");
|
||||||
|
var oFramesetMusicController = jsMVC.getController("framesetMusicController");
|
||||||
|
var sTitle = document.title;
|
||||||
|
|
||||||
|
var topFrameAlert = function(message){
|
||||||
|
alert(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
var topFrameConfirm = function(message){
|
||||||
|
if(confirm(message)){
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style type="text/css">
|
||||||
|
html{width:100%;height:100%;}
|
||||||
|
body{width:100%;height:100%;margin:0;padding:0;font-size:0;}
|
||||||
|
#mainFrame{width:100%;height:100%;margin:0;padding:0;border:0;}
|
||||||
|
#hiddenFrame{width:0;height:0;margin:0;padding:0;border:0;}
|
||||||
|
</style>
|
||||||
|
<body>
|
||||||
|
<iframe id="mainFrame" name="mainFrame" allowfullscreen="true"
|
||||||
|
src="/PostList.naver?blogId=iotdoor1749&widgetTypeCall=true&categoryNo=1&topReferer=https%3A%2F%2Fm.blog.naver.com%2FPostView.naver%3FblogId%3Diotdoor1749%26logNo%3D223606707258&trackingCode=blog_postview&directAccess=true"
|
||||||
|
scrolling="auto"
|
||||||
|
onload="oFramesetTitleController.start(self.frames['mainFrame'], self, sTitle);oFramesetTitleController.onLoadFrame();oFramesetUrlController.start(self.frames['mainFrame']);oFramesetUrlController.onLoadFrame();document.querySelector('#mainFrame').focus();"
|
||||||
|
allowfullscreen></iframe>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<!-- [D] XHTML 1.0 형식에 맞게 언어속성 추가-->
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang=“ko“ lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Pragma" content="no-cache"/>
|
||||||
|
<meta http-equiv="Expires" content="-1"/>
|
||||||
|
<meta name="robots" content="noindex,follow"/>
|
||||||
|
<meta name="referrer" content="always"/>
|
||||||
|
<meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="https://ssl.pstatic.net/static/blog/icon/favicon.ico" />
|
||||||
|
<link rel="alternate" type="application/rss+xml" href="https://rss.blog.naver.com/iotdoor1749.xml" title="RSS feed for iotdoor1749 Blog"/>
|
||||||
|
<link rel="wlwmanifest" type="application/wlwmanifest+xml" href="https://blog.naver.com/NBlogWlwLayout.naver?blogId=iotdoor1749" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<title>국제오토텍자동문 : 네이버 블로그</title>
|
||||||
|
</head>
|
||||||
|
<script type="text/javascript" src="https://ssl.pstatic.net/t.static.blog/mylog/versioning/Frameset-341345692_https.js" charset="UTF-8"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript" charset="UTF-8">
|
||||||
|
var photoContent="";
|
||||||
|
var postContent="";
|
||||||
|
|
||||||
|
var videoId = "";
|
||||||
|
var thumbnail = "";
|
||||||
|
var inKey = "";
|
||||||
|
var movieFileSize = "";
|
||||||
|
var playTime = "";
|
||||||
|
var screenSize = "";
|
||||||
|
|
||||||
|
var blogId = 'iotdoor1749';
|
||||||
|
var blogURL = 'https://blog.naver.com';
|
||||||
|
var eventCnt = '';
|
||||||
|
|
||||||
|
var g_ShareObject = {};
|
||||||
|
g_ShareObject.referer = "https%3A%2F%2Fm.blog.naver.com%2FPostView.naver%3FblogId%3Diotdoor1749%26logNo%3D223015406040";
|
||||||
|
|
||||||
|
|
||||||
|
jsMVC.setController("framesetTitleController", FramesetTitleController);
|
||||||
|
jsMVC.setController("framesetUrlController", FramesetUrlController);
|
||||||
|
jsMVC.setController("framesetMusicController", FramesetMusicController);
|
||||||
|
var oFramesetTitleController = jsMVC.getController("framesetTitleController");
|
||||||
|
var oFramesetUrlController = jsMVC.getController("framesetUrlController");
|
||||||
|
var oFramesetMusicController = jsMVC.getController("framesetMusicController");
|
||||||
|
var sTitle = document.title;
|
||||||
|
|
||||||
|
var topFrameAlert = function(message){
|
||||||
|
alert(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
var topFrameConfirm = function(message){
|
||||||
|
if(confirm(message)){
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style type="text/css">
|
||||||
|
html{width:100%;height:100%;}
|
||||||
|
body{width:100%;height:100%;margin:0;padding:0;font-size:0;}
|
||||||
|
#mainFrame{width:100%;height:100%;margin:0;padding:0;border:0;}
|
||||||
|
#hiddenFrame{width:0;height:0;margin:0;padding:0;border:0;}
|
||||||
|
</style>
|
||||||
|
<body>
|
||||||
|
<iframe id="mainFrame" name="mainFrame" allowfullscreen="true"
|
||||||
|
src="/PostList.naver?blogId=iotdoor1749&widgetTypeCall=true&categoryNo=1&topReferer=https%3A%2F%2Fm.blog.naver.com%2FPostView.naver%3FblogId%3Diotdoor1749%26logNo%3D223015406040&trackingCode=blog_postview&directAccess=true"
|
||||||
|
scrolling="auto"
|
||||||
|
onload="oFramesetTitleController.start(self.frames['mainFrame'], self, sTitle);oFramesetTitleController.onLoadFrame();oFramesetUrlController.start(self.frames['mainFrame']);oFramesetUrlController.onLoadFrame();document.querySelector('#mainFrame').focus();"
|
||||||
|
allowfullscreen></iframe>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 417 KiB |
|
After Width: | Height: | Size: 276 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 290 KiB |
|
After Width: | Height: | Size: 308 KiB |
|
After Width: | Height: | Size: 290 KiB |
@@ -0,0 +1,81 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<!-- [D] XHTML 1.0 형식에 맞게 언어속성 추가-->
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang=“ko“ lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Pragma" content="no-cache"/>
|
||||||
|
<meta http-equiv="Expires" content="-1"/>
|
||||||
|
<meta name="robots" content="noindex,follow"/>
|
||||||
|
<meta name="referrer" content="always"/>
|
||||||
|
<meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="https://ssl.pstatic.net/static/blog/icon/favicon.ico" />
|
||||||
|
<link rel="alternate" type="application/rss+xml" href="https://rss.blog.naver.com/iotdoor1749.xml" title="RSS feed for iotdoor1749 Blog"/>
|
||||||
|
<link rel="wlwmanifest" type="application/wlwmanifest+xml" href="https://blog.naver.com/NBlogWlwLayout.naver?blogId=iotdoor1749" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<title>국제오토텍자동문 : 네이버 블로그</title>
|
||||||
|
</head>
|
||||||
|
<script type="text/javascript" src="https://ssl.pstatic.net/t.static.blog/mylog/versioning/Frameset-341345692_https.js" charset="UTF-8"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript" charset="UTF-8">
|
||||||
|
var photoContent="";
|
||||||
|
var postContent="";
|
||||||
|
|
||||||
|
var videoId = "";
|
||||||
|
var thumbnail = "";
|
||||||
|
var inKey = "";
|
||||||
|
var movieFileSize = "";
|
||||||
|
var playTime = "";
|
||||||
|
var screenSize = "";
|
||||||
|
|
||||||
|
var blogId = 'iotdoor1749';
|
||||||
|
var blogURL = 'https://blog.naver.com';
|
||||||
|
var eventCnt = '';
|
||||||
|
|
||||||
|
var g_ShareObject = {};
|
||||||
|
g_ShareObject.referer = "https%3A%2F%2Fm.blog.naver.com%2FPostView.naver%3FblogId%3Diotdoor1749%26logNo%3D223280145952";
|
||||||
|
|
||||||
|
|
||||||
|
jsMVC.setController("framesetTitleController", FramesetTitleController);
|
||||||
|
jsMVC.setController("framesetUrlController", FramesetUrlController);
|
||||||
|
jsMVC.setController("framesetMusicController", FramesetMusicController);
|
||||||
|
var oFramesetTitleController = jsMVC.getController("framesetTitleController");
|
||||||
|
var oFramesetUrlController = jsMVC.getController("framesetUrlController");
|
||||||
|
var oFramesetMusicController = jsMVC.getController("framesetMusicController");
|
||||||
|
var sTitle = document.title;
|
||||||
|
|
||||||
|
var topFrameAlert = function(message){
|
||||||
|
alert(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
var topFrameConfirm = function(message){
|
||||||
|
if(confirm(message)){
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style type="text/css">
|
||||||
|
html{width:100%;height:100%;}
|
||||||
|
body{width:100%;height:100%;margin:0;padding:0;font-size:0;}
|
||||||
|
#mainFrame{width:100%;height:100%;margin:0;padding:0;border:0;}
|
||||||
|
#hiddenFrame{width:0;height:0;margin:0;padding:0;border:0;}
|
||||||
|
</style>
|
||||||
|
<body>
|
||||||
|
<iframe id="mainFrame" name="mainFrame" allowfullscreen="true"
|
||||||
|
src="/PostList.naver?blogId=iotdoor1749&widgetTypeCall=true&categoryNo=1&topReferer=https%3A%2F%2Fm.blog.naver.com%2FPostView.naver%3FblogId%3Diotdoor1749%26logNo%3D223280145952&trackingCode=blog_postview&directAccess=true"
|
||||||
|
scrolling="auto"
|
||||||
|
onload="oFramesetTitleController.start(self.frames['mainFrame'], self, sTitle);oFramesetTitleController.onLoadFrame();oFramesetUrlController.start(self.frames['mainFrame']);oFramesetUrlController.onLoadFrame();document.querySelector('#mainFrame').focus();"
|
||||||
|
allowfullscreen></iframe>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 287 KiB |