카테고리 관리 ui 변경

This commit is contained in:
2026-02-20 14:28:04 +09:00
parent 23e39d8b65
commit d56e346409
4 changed files with 838 additions and 568 deletions

View File

@@ -34,6 +34,7 @@
SELECT CATEGORY_NO
, CATEGORY_DIV_CD
, CATEGORY_NM
, ORDER_NO
, DATE_FORMAT(REG_DATE, '%Y-%m-%d %H:%i') AS REG_DATE
FROM madeu.HP_CATEGORY
<where>
@@ -64,14 +65,18 @@
</select>
<insert id="putCategoryManagement" parameterType="hashmap">
<selectKey resultType="string" keyProperty="categoryNo" order="BEFORE">
SELECT NVL(MAX(CATEGORY_NO),0) + 1 FROM HP_CATEGORY WHERE CATEGORY_DIV_CD = #{categoryDivCd}
<selectKey resultType="hashmap" keyProperty="categoryNo,orderNo" order="BEFORE">
SELECT IFNULL(MAX(CATEGORY_NO),0) + 1 as categoryNo
, CASE WHEN #{orderNo} IS NULL OR #{orderNo} = '' OR #{orderNo} = 0 THEN IFNULL(MAX(ORDER_NO),0) + 1 ELSE #{orderNo} END as orderNo
FROM HP_CATEGORY
WHERE CATEGORY_DIV_CD = #{categoryDivCd}
</selectKey>
/** CategoryManagementSql.putCategoryManagement **/
INSERT INTO HP_CATEGORY(
CATEGORY_NO
, CATEGORY_DIV_CD
, CATEGORY_NM
, ORDER_NO
, USE_YN
, REG_ID
, REG_DATE
@@ -81,6 +86,7 @@
#{categoryNo},
#{categoryDivCd},
#{categoryNm},
#{orderNo},
'Y',
#{regId},
NOW(),
@@ -93,6 +99,7 @@
/** CategoryManagementSql.modCategoryManagement **/
UPDATE HP_CATEGORY
SET CATEGORY_NM = #{categoryNm}
,ORDER_NO = #{orderNo}
,MOD_ID = #{modId}
,MOD_DATE = NOW()
WHERE CATEGORY_NO = #{categoryNo}

View File

@@ -0,0 +1,399 @@
/* ===================================================================
카테고리 트리 레이아웃 스타일
=================================================================== */
/* 트리 + 상세 패널 레이아웃 */
.category-tree-layout {
display: flex;
width: 100%;
height: calc(100% - 140px);
gap: 12px;
}
/* ---- 좌측 트리 패널 ---- */
.category-tree-panel {
width: 380px;
min-width: 300px;
background: #fff;
border: 1px solid #E9ECF0;
border-radius: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.tree-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid #E9ECF0;
background: #fff;
/* 밝게 변경 */
}
.tree-panel-title {
font-size: 14px;
font-weight: 700;
color: #333;
}
.tree-panel-actions {
display: flex;
gap: 6px;
}
.tree-sub-header {
display: flex;
gap: 6px;
padding: 8px 12px;
border-bottom: 1px solid #E9ECF0;
justify-content: flex-start;
/* Changed to Left Align */
background: #fff;
align-items: center;
}
.tree-action-btn {
padding: 2px 8px;
font-size: 12px;
color: #666;
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
/* Button styles in Tree Header */
.tree-panel-actions,
.tree-sub-header {
display: flex;
gap: 4px;
align-items: center;
}
.tree-panel-actions .tree-action-btn,
.tree-panel-actions .put_btn,
.tree-panel-actions .delete_btn,
.tree-sub-header .tree-action-btn {
float: none !important;
margin: 0 !important;
padding: 2px 8px;
font-size: 12px;
height: 26px;
/* Unified height */
line-height: normal;
border-radius: 4px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
cursor: pointer;
}
.tree-panel-actions .put_btn img,
.tree-panel-actions .delete_btn img {
width: 12px;
height: 12px;
margin: 0;
position: static !important;
}
/* Specific colors */
.tree-panel-actions .put_btn {
background: #3985EA;
color: #fff;
border: 1px solid #3985EA;
}
.tree-panel-actions .delete_btn {
background: #FF2222;
color: #fff;
border: 1px solid #FF2222;
}
.tree-panel-actions .tree-action-btn {
background: #fff;
color: #666;
border: 1px solid #ddd;
}
.tree-action-btn:hover {
background: #3985EA;
/* Primary Blue */
color: #fff;
border-color: #3985EA;
}
.category-tree-container {
flex: 1;
overflow: hidden;
/* Changed from auto to hidden to prevent double scrollbars with Tabulator */
padding: 0;
/* Padding 제거 (Tabulator가 꽉 차게) */
}
#categoryTreeContainer {
height: 100%;
width: 100%;
}
/* Active Row Highlight */
.tabulator-row.row-active {
background-color: #e6f7ff !important;
/* Light Blue */
font-weight: bold;
color: #1890ff;
}
/* ---- 우측 상세 패널 ---- */
.category-detail-panel {
flex: 1;
background: #fff;
border: 1px solid #E9ECF0;
border-radius: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.detail-panel-header {
display: flex;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #E9ECF0;
background: #fff;
/* 밝게 변경 */
}
.detail-panel-title {
font-size: 14px;
font-weight: 700;
color: #333;
}
.detail-panel-content {
flex: 1;
padding: 20px;
overflow: auto;
}
.detail-empty-state {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #999;
font-size: 14px;
}
/* 폼 스타일 */
.detail-form-group {
margin-bottom: 16px;
}
.detail-form-group label {
display: block;
margin-bottom: 6px;
font-size: 13px;
font-weight: 600;
color: #555;
}
.detail-form-input,
.detail-form-select {
width: 100%;
height: 38px;
padding: 0 12px;
border: 1px solid #E9ECF0;
border-radius: 5px;
font-size: 14px;
color: #333;
background: #fff;
transition: border-color 0.2s;
}
.detail-form-input:focus,
.detail-form-select:focus {
border-color: #3985EA;
outline: none;
box-shadow: 0 0 0 3px rgba(57, 133, 234, 0.1);
}
.detail-form-input[readonly] {
background: #f5f5f5;
color: #888;
}
.detail-form-select {
appearance: none;
background: #fff url('/image/web/select_arrow.svg') no-repeat right 10px center;
cursor: pointer;
}
.detail-form-actions {
display: flex;
gap: 8px;
margin-top: 24px;
}
.detail-save-btn {
padding: 8px 24px;
background: #3985EA;
color: #fff;
border: none;
border-radius: 5px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.detail-save-btn:hover {
background: #2c6fd1;
}
.detail-cancel-btn {
padding: 8px 24px;
background: #fff;
color: #666;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.detail-cancel-btn:hover {
background: #f5f5f5;
border-color: #ccc;
}
/* 상세정보 표시 */
.detail-info-group {
margin-bottom: 14px;
}
.detail-info-label {
font-size: 12px;
font-weight: 600;
color: #999;
margin-bottom: 4px;
}
.detail-info-value {
font-size: 14px;
color: #333;
padding: 8px 12px;
background: #f8f9fb;
border-radius: 4px;
}
/* ===================================================================
Tabulator Custom Styling (Light Theme)
=================================================================== */
.tabulator {
border: none;
background-color: #fff;
}
/* Header */
.tabulator .tabulator-header {
background-color: #fff;
border-bottom: 1px solid #E9ECF0;
font-weight: 600;
color: #494E53;
}
.tabulator .tabulator-header .tabulator-col {
background-color: #fff;
border-right: 1px solid #E9ECF0;
}
.tabulator .tabulator-header .tabulator-col-content {
padding: 10px;
}
/* Rows */
.tabulator .tabulator-row {
background-color: #fff;
border-bottom: 1px solid #E9ECF0;
color: #333;
}
.tabulator .tabulator-row:nth-child(even) {
background-color: #fff;
/* Zebra striping 제거 혹은 연하게 */
}
.tabulator .tabulator-row:hover {
background-color: rgba(57, 133, 234, 0.05) !important;
}
.tabulator .tabulator-row.tabulator-selected {
background-color: rgba(57, 133, 234, 0.1) !important;
border-color: #3985EA;
}
.tabulator .tabulator-cell {
padding: 8px 10px;
border-right: 1px solid #E9ECF0;
font-size: 14px;
}
/* Group Headers */
.tabulator .tabulator-group {
background-color: #f8f9fb !important;
border-bottom: 1px solid #E9ECF0;
border-top: 1px solid #E9ECF0;
color: #2c3e50;
font-weight: 700;
padding: 8px 10px;
}
.tabulator .tabulator-group:hover {
background-color: #f0f2f5 !important;
cursor: pointer;
}
/* Checkbox (Row Selection) */
.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title input[type=checkbox],
.tabulator .tabulator-cell input[type=checkbox] {
accent-color: #3985EA;
width: 16px;
height: 16px;
cursor: pointer;
}
/* ---- 반응형 ---- */
@media only screen and (max-width:1280px) {
.category-tree-panel {
width: 280px;
min-width: 240px;
}
.category-tree-layout {
height: calc(100% - 120px);
}
}
@media only screen and (max-width:1080px) {
.category-tree-layout {
flex-direction: column;
height: auto;
}
.category-tree-panel {
width: 100%;
height: 300px;
}
.category-detail-panel {
height: 300px;
}
}

View File

@@ -1,34 +1,88 @@
/* 페이징 관련 변수 */
let webCategoryTotalCount = 0;
let webCategoryTotalPages = 0;
/**
* CategoryManagement.js
* Tabulator library based implementation
*/
/*aggird*/
let webCategoryAgGridData = [];
// Global Table Instance
let categoryTable = null;
let categoryList = []; // Full flat list from server
/* 삭제할 항목들 */
let delList = [];
// Category division map for labels
const categoryDivMap = {
"01": "다이어트 시술",
"02": "다이어트 이벤트",
"03": "쁘띠 시술",
"04": "쁘띠 이벤트",
"05": "다이어트 전후사진",
"06": "쁘띠 전후사진"
};
/* 등록 버튼 이중 클릭 방지 플래그 */
let isInsertBtnDisabled = false;
// Initialize
$(function () {
fn_init();
fn_initEvent();
});
/****************************************************************************
* 카테고리 정보 리스트 조회
****************************************************************************/
function fn_selectListwebCategoryJson(){
function fn_init() {
// Initial load
fn_searchCategoryList();
}
function fn_initEvent() {
// Expand/Collapse All (Groups)
$("#btnExpandAll").click(function () {
if (categoryTable) {
console.log("Expanding all groups...");
categoryTable.blockRedraw();
categoryTable.getGroups().forEach(group => group.show());
categoryTable.restoreRedraw();
}
});
$("#btnCollapseAll").click(function () {
if (categoryTable) {
console.log("Collapsing all groups...");
categoryTable.blockRedraw();
categoryTable.getGroups().forEach(group => group.hide());
categoryTable.restoreRedraw();
}
});
// CRUD Buttons (Top)
$("#btnInsertWebCategory").click(function () {
fn_showInsertForm();
});
$("#btnDeleteWebCategory").click(function () {
fn_deleteCategory();
});
// Form Buttons
$("#btnSaveCategory").click(fn_insertCategory);
$("#btnCancelInsert").click(fn_hideDetailPanel);
$("#btnUpdateCategory").click(fn_updateCategory);
$("#btnCancelEdit").click(fn_hideDetailPanel);
}
/**
* Fetch Data
*/
function fn_searchCategoryList() {
let formData = new FormData();
formData.append("menuClass", menuClass);
// gridSort에 categoryNm가 들어오면 CATEGORY_NM로 변환
let sortValue = webCategorySort;
if (sortValue && sortValue.indexOf('categoryNm') !== -1) {
sortValue = sortValue.replace(/categoryNm/g, 'CATEGORY_NM');
}
formData.append("gridSort", sortValue);
formData.append("gridLimitStart", webCategoryStart || 0);
formData.append("gridLimitEnd", webCategoryLimit || 10);
formData.append("categoryDivCd", categoryDivCd);
formData.append("categoryNm", categoryNm);
formData.append("searchCategoryDivCd", $("#searchCategoryDivCd").val());
formData.append("searchCategoryNm", $("#searchCategoryNm").val());
// Server expects gridLimitStart/End for pagination, sending large range for All data
formData.append("gridLimitStart", 0);
formData.append("gridLimitEnd", 100000);
// Search filters (Removed)
formData.append("searchCategoryDivCd", "");
formData.append("searchCategoryNm", "");
// Sort
formData.append("gridSort", "CATEGORY_DIV_CD ASC, CATEGORY_NM ASC");
$.ajax({
url: encodeURI('/categoryManagement/getCategoryManagementList.do'),
@@ -37,513 +91,277 @@ function fn_selectListwebCategoryJson(){
processData: false,
contentType: false,
type: 'POST',
async: true,
success: function(data){
if('0'==data.msgCode){
// 페이징 처리
webCategoryTotalCount = data.totalCount;
//$("#txt_noticeTotalCount").text(noticeTotalCount);
webCategoryTotalPages = Math.ceil(webCategoryTotalCount/webCategoryLimit);
// 리스트 조회
webCategoryAgGridData = data.rows;
webCategoryGridOptions.api.setRowData(webCategoryAgGridData);
if(0<data.rows.length){
//페이징 처리
window.pagObj = $('#webCategoryPagination').twbsPagination({
startPage : ((webCategoryStart/webCategoryLimit)+1),
totalPages : (webCategoryTotalPages==0)?1:webCategoryTotalPages,
visiblePages : 10,
initiateStartPageClick: false,
prev : '<img src="/image/web/page_navigation_arrow.svg" alt="prev"/>',
next : '<img src="/image/web/page_navigation_arrow.svg" alt="next"/>',
first : '',
last : '',
onPageClick: function (Category, page) {
fn_webCategoryPagination(page);
}
}).on('page', function (Category, page) {
//console.info(page + ' (from Category listening)');
});
}
else{
}
}
else{
modalEvent.danger("조회 오류", data.msgDesc);
success: function (data) {
if (data.msgCode === '0' || data.success === 'true') {
categoryList = data.rows || [];
fn_renderTable(categoryList);
fn_hideDetailPanel();
} else {
alert(data.msgDesc || "조회 중 오류가 발생했습니다.");
}
},
error : function(xhr, status, error) {
modalEvent.danger("조회 오류", "조회 중 오류가 발생하였습니다. 잠시후 다시시도하십시오.");
},
beforeSend:function(){
// 로딩열기
webCategoryGridOptions.api.showLoadingOverlay();
},
complete:function(){
error: function () {
alert("서버 통신 오류가 발생습니다.");
}
});
}
/****************************************************************************
* 검색하기
****************************************************************************/
function fn_webCategorySearch(param){
if("A"!=param && "Y"!=selectUseYn){
modalEvent.warning("", "조회 권한이 없습니다.");
return false;
}
// Render Tabulator
function fn_renderTable(list) {
// Pre-process list to strip HTML tags for display if needed,
// or we can use a formatter. Let's use a simple formatter.
fn_webCategoryPaginReset();
categoryDivCd = $("#searchCategoryDivCd").val();
categoryNm = $("#searchCategoryNm").val();
fn_selectListwebCategoryJson();
}
/****************************************************************************
* 초기화하기
****************************************************************************/
function fn_webCategoryReset(){
$("#searchCategoryDivCd").val("");
$("#searchCategoryNm").val("");
fn_webCategorySearch();
}
/****************************************************************************
* 페이징 처리
****************************************************************************/
function fn_webCategoryPagination(param){
webCategoryStart = (parseInt(param)-1)*webCategoryLimit;
fn_selectListwebCategoryJson();
}
/****************************************************************************
* 페이징 리셋
****************************************************************************/
function fn_webCategoryPaginReset(){
branchOfficeCd = '';
mbName = '';
mbHp = '';
opinionClassificationCd = '';
webCategoryStart = 0;
webCategoryLimit = 100;
webCategoryTotalCount = 0;
webCategoryTotalPages = 0;
//페이징 초기화
if($("#webCategoryPagination").data("twbs-pagination")){
$("#webCategoryPagination").twbsPagination("destroy");
}
}
/****************************************************************************
* 검색 엔터 카테고리
****************************************************************************/
function fn_webCategoryEnter(e){
if(e.which){
// 파이어폭스
if(13 == e.which) {
//로그인 액션 스크립트
fn_webCategorySearch();
}
}
else{
// 윈도우, 사파리, 크롬
if(13 == Category.keyCode) {
//로그인 액션 스크립트
fn_webCategorySearch();
}
}
}
/****************************************************************************
* 완료
****************************************************************************/
function fn_webCategoryOk(){
isInsertBtnDisabled = false;
$("#btnInsertWebCategory").prop('disabled', false);
fn_webCategoryReset();
}
// JavaScript
// Category division options
let categoryDivOptions = [
{ value: "01", label: "다이어트 시술" },
{ value: "02", label: "다이어트 이벤트" },
{ value: "03", label: "쁘띠 시술" },
{ value: "04", label: "쁘띠 이벤트" },
{ value: "05", label: "다이어트 전후사진" },
{ value: "06", label: "쁘띠 전후사진" }
];
let webCategoryColumnDefs = [
{field: "checkbox", headerName:"", minWidth:55, maxWidth:55, headerCheckboxSelection: true, checkboxSelection: true},
categoryTable = new Tabulator("#categoryTreeContainer", {
data: list,
layout: "fitColumns",
selectableRows: false, // Updated: Only select via checkbox
groupBy: "categoryDivCd",
groupStartOpen: true, // Default to Expanded
groupHeader: function (value, count, data, group) {
// Custom Header: "Division Name (Count)"
let label = categoryDivMap[value] || value;
return label + " (" + count + ")";
},
columns: [
{
field: "categoryDivCd",
headerName: "카테고리구분",
minWidth: 150,
maxWidth: 200,
cellStyle: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
},
cellRenderer: function(params) {
let found = categoryDivOptions.find(opt => opt.value === params.value);
return found ? found.label : params.value;
},
// Make editable only for new rows (regDate is empty)
editable: function(params) {
// 신규행(regDate 없음)이고 등록중 플래그가 있거나, 둘 중 하나라도 값이 없으면 false
if (!params.data.regDate) {
if (params.data._isRegistering) return false;
// 둘 중 하나라도 값이 없으면(입력 중)만 true, 둘 다 값이 있으면 false
if (!params.data.categoryDivCd || !params.data.categoryNm) return true;
return false;
}
// 다른 행: 그리드에 신규행이 있고, 그 신규행이 입력 중이거나 등록중이면 수정 불가
let rowData = [];
params.api.forEachNode(function(node) {
rowData.push(node.data);
});
let hasNewRowEditing = rowData.some(row => !row.regDate && (!row.categoryDivCd || !row.categoryNm || row._isRegistering));
if (hasNewRowEditing) return false;
return true;
},
cellEditor: 'agSelectCellEditor',
cellEditorParams: {
values: categoryDivOptions.map(opt => opt.label)
},
valueSetter: function(params) {
let found = categoryDivOptions.find(opt => opt.label === params.newValue);
if (found) {
params.data.categoryDivCd = found.value;
return true;
}
return false;
formatter: "rowSelection",
titleFormatter: "rowSelection",
headerSort: false,
width: 40,
hozAlign: "center",
headerHozAlign: "center",
cellClick: function (e, cell) {
cell.getRow().toggleSelect();
}
},
{
title: "카테고리명",
field: "categoryNm",
headerName: "카테고리명",
minWidth: 200,
maxWidth: 300,
cellStyle: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
headerSort: false,
formatter: function (cell) {
// Strip HTML tags
let val = cell.getValue();
return val ? String(val).replace(/<[^>]*>?/gm, '') : '';
},
// 신규행이 등록중이거나 입력이 덜 된 경우 수정 불가
editable: function(params) {
// 신규행: 입력(값이 없을 때)만 가능, 입력이 끝나면(둘 다 값이 있으면) 수정 불가, 등록중에도 수정 불가
if (!params.data.regDate) {
if (params.data._isRegistering) return false;
// 둘 중 하나라도 값이 없으면 true(입력 가능), 둘 다 값이 있으면 false(수정 불가)
if (!params.data.categoryDivCd || !params.data.categoryNm) return true;
return false;
cellClick: function (e, cell) {
// Handle Cell Click -> Edit
const row = cell.getRow();
const item = row.getData();
// Highlight Row
if (categoryTable) {
const rows = categoryTable.getRows();
rows.forEach(r => r.getElement().classList.remove("row-active"));
row.getElement().classList.add("row-active");
}
// 신규행이 등록중이거나 입력 중이면 다른 행은 수정 불가
let rowData = [];
params.api.forEachNode(function(node) {
rowData.push(node.data);
console.log("Name Cell Clicked:", item);
fn_showEditForm(item);
}
}
],
// rowClick was causing issues with selection/edit overlap.
// Moved to specific cellClick handlers.
});
let hasNewRowEditing = rowData.some(row => !row.regDate && (!row.categoryDivCd || !row.categoryNm || row._isRegistering));
if (hasNewRowEditing) return false;
return true;
}
},
{ field: "regDate", headerName:"등록일", minWidth: 130, maxWidth: 150, editable: false,
cellStyle: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
},
}
];
}
// let the grid know which columns and what data to use
let webCategoryGridOptions = {
suppressRowTransform: true,
columnDefs: webCategoryColumnDefs,
defaultColDef: { // 리스트 기본 설정
flex: 1,
sortable: true, //정렬 여부
resizable: true, //리사이즈
cellStyle:{
textAlign:'left',
fontSize:'14px',
lineHeight:'16px',
whiteSpace:'normal',
padding:'0'
},
//suppressSizeToFit:true, //자동 맞춤
//enableRowGroup: true, // 그룹 묶음
enablePivot: true,
enableValue : true,
wrapText: true, // 텍스트 줄바꿈
autoHeight: true
},
//suppressMultiSort:true, //단일솔트 true가 단일, false가 다중 shift + sort 시
headerHeight : 41, // header 높이
//rowHeight : 41, // row 높이
rowData : webCategoryAgGridData,
suppressRowClickSelection : true, // 로우 클릭시 체크박스 선택 true no, false yes
localeText : {
noRowsToShow : '조회 결과가 없습니다.'
}, //데이터 없을 시 나오는 문구
rowSelection : 'multiple', // row 다중 선택
debug : false,
onSelectionChanged: function(Category){
delList = Category.api.getSelectedRows();
},
onSortChanged: function(Category){
//정렬
webCategorySort = ''; //기존 정렬 초기화
let columnArr = Category.columnApi.getColumnState();
if(0<columnArr.length){
//sort index 순으로 재정렬
columnArr.sort(function(a,b){
return a.sortIndex - b.sortIndex;
});
let nullCnt = 0;
for(let i=0; i<columnArr.length; i++){
let gridSortModel = columnArr[i].colId;
let gridSort = columnArr[i].sort;
/**
* Detail Panel: Hide All
*/
function fn_hideDetailPanel() {
$("#categoryInsertForm").hide();
$("#categoryEditForm").hide();
$("#categoryDetailContent").show(); // Default empty state
// Clear selection if desired
// if(categoryTable) categoryTable.deselectRow(); // Optional: keep selection
}
if(gridSort != null){
webCategoryStart = 0;
webCategorySort += gridSortModel+' '+ gridSort + ',';
/**
* Detail Panel: Show Insert
*/
function fn_showInsertForm(preSelectDivCd) {
if ("Y" !== insertUseYn) {
alert("등록 권한이 없습니다.");
return;
}
else{
nullCnt++;
if(nullCnt == columnArr.length){
webCategorySort = '';
webCategoryDir = '';
webCategoryStart = 0;
}
}
}
}
webCategorySort = webCategorySort.substring(0,webCategorySort.lastIndexOf( ",")); //맨끝 콤마 지우기
fn_webCategorySearch();
},
onCellValueChanged: function(event) {
// 신규 row: regDate가 없을 때, 둘 다 값이 있어야 등록
if (!event.data.regDate) {
if (event.data.categoryDivCd && event.data.categoryNm) {
// 등록중 플래그 설정
event.data._isRegistering = true;
event.api.refreshCells({ force: true }); // 색상 즉시 반영
let data = {
categoryDivCd: event.data.categoryDivCd,
categoryNm: event.data.categoryNm,
$("#categoryDetailContent").hide();
$("#categoryEditForm").hide();
$("#categoryInsertForm").show();
// Reset fields
$("#insertCategoryDivCd").val(preSelectDivCd || "");
$("#insertCategoryNm").val("");
// Order No is auto-generated (MAX+1) on server
// Update Header
$(".detail-panel-title").text("카테고리 등록");
}
/**
* Detail Panel: Show Edit
*/
function fn_showEditForm(item) {
if (!item) return;
console.log("Show Edit Form for:", item);
try {
$("#categoryDetailContent").hide();
$("#categoryInsertForm").hide();
$("#categoryEditForm").show();
// Fill fields
$("#editCategoryNo").val(item.categoryNo); // PK
$("#editCategoryDivCd").val(item.categoryDivCd); // PK part
$("#editCategoryDivNm").val(categoryDivMap[item.categoryDivCd] || item.categoryDivCd);
// Strip HTML for input value as well? usually we want the raw value if it is editable.
// Assuming edit is for clean text.
let cleanName = item.categoryNm ? String(item.categoryNm).replace(/<[^>]*>?/gm, '') : '';
$("#editCategoryNm").val(cleanName);
$("#editOrderNo").val(item.orderNo || "0"); // Set Order No
$("#editRegDate").val(item.regDate);
// Update Header
$(".detail-panel-title").text("카테고리 상세/수정");
} catch (err) {
console.error("Error displaying edit form:", err);
}
}
/**
* Action: Insert
*/
function fn_insertCategory() {
const divCd = $("#insertCategoryDivCd").val();
const nm = $("#insertCategoryNm").val();
// Order No handled by backend (MAX+1)
if (!divCd) { alert("카테고리 구분을 선택해주세요."); return; }
if (!nm) { alert("카테고리명을 입력해주세요."); return; }
const data = {
categoryDivCd: divCd,
categoryNm: nm,
orderNo: 0, // Default 0 to trigger backend logic
menuClass: menuClass
};
$.ajax({
url: '/categoryManagement/putCategoryManagement.do',
type: 'POST',
data: JSON.stringify(data),
contentType: 'application/json',
success: function(res) {
if (typeof res === 'string') {
try {
res = JSON.parse(res);
} catch (e) {
modalEvent.danger('등록 오류', '서버 응답 파싱 오류');
event.data._isRegistering = false;
return;
}
}
if (res.msgCode === '0') {
modalEvent.success('등록 완료', '카테고리가 등록되었습니다.', function(){
fn_webCategoryOk();
});
success: function (res) {
let response = (typeof res === 'string') ? JSON.parse(res) : res;
if (response.msgCode === '0') {
alert("등록되었습니다.");
fn_searchCategoryList(); // reload
} else {
modalEvent.danger('등록 오류', res.msgDesc);
alert(response.msgDesc);
}
event.data._isRegistering = false;
event.api.refreshCells({ force: true }); // 색상 즉시 반영
},
error: function() {
modalEvent.danger('등록 오류', '등록 중 오류가 발생하였습니다.');
event.data._isRegistering = false;
event.api.refreshCells({ force: true }); // 색상 즉시 반영
error: function () {
alert("등록 중 오류가 발생습니다.");
}
});
} else {
// 둘 중 하나라도 없으면 등록/수정 불가, 아무 동작 안함
}
/**
* Action: Update
*/
function fn_updateCategory() {
if ("Y" !== updateUseYn) {
alert("수정 권한이 없습니다.");
return;
}
} else {
// 기존 row 수정: 둘 다 값이 있을 때만 수정
if (event.data.categoryDivCd && event.data.categoryNm) {
let updatedData = { ...event.oldData, ...event.data };
updatedData.menuClass = menuClass;
const no = $("#editCategoryNo").val();
const divCd = $("#editCategoryDivCd").val();
const nm = $("#editCategoryNm").val();
const orderNo = $("#editOrderNo").val() || 0;
if (!nm) { alert("카테고리명을 입력해주세요."); return; }
const data = {
categoryNo: no,
categoryDivCd: divCd,
categoryNm: nm,
orderNo: orderNo,
menuClass: menuClass
};
$.ajax({
url: '/categoryManagement/modCategoryManagement.do',
type: 'POST',
data: JSON.stringify(updatedData),
data: JSON.stringify(data),
contentType: 'application/json',
success: function(res) {
response = JSON.parse(res);
success: function (res) {
let response = (typeof res === 'string') ? JSON.parse(res) : res;
if (response.msgCode === '0') {
modalEvent.success('수정 완료', '수정이 성공적으로 반영되었습니다.');
fn_selectListwebCategoryJson();
alert("수정되었습니다.");
fn_searchCategoryList();
} else {
modalEvent.danger('수정 오류', response.msgDesc);
alert(response.msgDesc);
}
},
error: function() {
modalEvent.danger('수정 오류', '수정 중 오류가 발생하였습니다.');
error: function () {
alert("수정 중 오류가 발생습니다.");
}
});
} else {
// 둘 중 하나라도 없으면 수정 불가, 아무 동작 안함
}
/**
* Action: Delete
*/
function fn_deleteCategory() {
if ("Y" !== deleteUseYn) {
alert("삭제 권한이 없습니다.");
return;
}
if (!categoryTable) return;
// Get Selected Data
const selectedData = categoryTable.getSelectedData();
if (selectedData.length === 0) {
alert("삭제할 카테고리를 선택해주세요.");
return;
}
},
getRowStyle: function(params) {
return null; // 색상은 CSS에서 처리
},
getRowClass: function(params) {
if (!params.data) return '';
if (!params.data.regDate && params.data._isRegistering) {
return 'registering-row';
}
if (!params.data.regDate) {
return 'new-row';
}
return '';
}
};
// lookup the container we want the Grid to use
let webCategoryGridDiv = document.querySelector('#webCategoryGrid');
// Map to required format
const itemsToDelete = selectedData.map(item => ({
categoryNo: item.categoryNo,
categoryDivCd: item.categoryDivCd
}));
// create the grid passing in the div to use together with the columns & data we want to use
new agGrid.Grid(webCategoryGridDiv, webCategoryGridOptions);
/****************************************************************************
* 페이지 init
****************************************************************************/
function fn_pageInit(){
// 초기 페이징 처리
$("#searchCategoryDivCd").val(categoryDivCd);
$("#searchCategoryNm").val(categoryNm);
fn_webCategorySearch("A");
}
/****************************************************************************
* 페이지 Category
****************************************************************************/
function fn_pageCategory(){
$(document).on('keypress', '#searchCategoryDivCd', function(e) {
fn_webCategoryEnter(e);
});
$(document).on('keypress', '#searchCategoryNm', function(e) {
fn_webCategoryEnter(e);
});
$("#btnSearchWebCategory").click(function () {
fn_webCategorySearch();
});
$("#btnInsertWebCategory").click(function () {
if (isInsertBtnDisabled) return;
isInsertBtnDisabled = true;
$(this).prop('disabled', true);
fn_insertwebCategoryIntro();
// 등록 완료 또는 실패 시 다시 활성화 (fn_webCategoryOk에서 처리)
});
$("#btnDeleteWebCategory").click(function () {
fn_deleteWebCategory();
});
}
// 카테고리 등록 모달 열기
function fn_insertwebCategoryIntro() {
if("Y"==insertUseYn){
// 그리드에 한 줄 추가
let newRow = {
categoryDivCd: '',
categoryNm: '',
regDate: ''
if (confirm(itemsToDelete.length + "건의 카테고리를 삭제하시겠습니까?")) {
const data = {
delList: itemsToDelete,
menuClass: menuClass
};
webCategoryGridOptions.api.applyTransaction({ add: [newRow], addIndex: 0 });
// 첫 번째 row, 카테고리구분 셀에 포커스
setTimeout(function() {
webCategoryGridOptions.api.startEditingCell({
rowIndex: 0,
colKey: 'categoryDivCd'
});
}, 100);
} else {
modalEvent.warning('', '등록 권한이 없습니다.');
return false;
}
}
// 카테고리 삭제
function fn_deleteWebCategory() {
// 신규행이 등록중이거나 입력 중이면 삭제 불가
let rowData = [];
webCategoryGridOptions.api.forEachNode(function(node) {
rowData.push(node.data);
});
let hasNewRowEditing = rowData.some(row => !row.regDate && (!row.categoryDivCd || !row.categoryNm || row._isRegistering));
if (hasNewRowEditing) {
modalEvent.warning('', '신규 카테고리 등록이 완료되기 전에는 삭제할 수 없습니다.');
return;
}
if(!delList || delList.length === 0) {
modalEvent.warning('', '삭제할 카테고리를 선택하세요.');
return;
}
modalEvent.info('삭제', '선택한 카테고리를 삭제하시겠습니까?', function(){
let data = {
menuClass: menuClass,
delList: delList
};
$.ajax({
url: '/categoryManagement/delCategoryManagement.do',
data: JSON.stringify(data),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
type: 'POST',
async: true,
success: function(res){
if(res.msgCode === '0'){
modalEvent.success('삭제 완료', '카테고리가 삭제되었습니다.', function(){
fn_webCategoryOk();
});
success: function (res) {
let response = (typeof res === 'string') ? JSON.parse(res) : res;
if (response.msgCode === '0') {
alert("삭제되었습니다.");
fn_searchCategoryList();
} else {
modalEvent.danger('삭제 오류', res.msgDesc);
alert(response.msgDesc);
}
},
error: function(xhr, status, error) {
modalEvent.danger('삭제 오류', '삭제 중 오류가 발생하였습니다. 잠시후 다시시도하십시오.');
error: function () {
alert("삭제 중 오류가 발생습니다.");
}
});
});
}
}
$(function(){
// 페이지 init
fn_pageInit();
// 페이지 Category
fn_pageCategory();
});

View File

@@ -1,36 +1,27 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{/web/layout/homeLayout}">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{/web/layout/homeLayout}">
<th:block layout:fragment="layout_css">
<link rel="stylesheet" href="/css/web/webCategorySelectList.css?v1.1">
<link rel="stylesheet" href="/css/web/webCategorySelectList.css?v2.0">
<link rel="stylesheet" href="/css/web/grid.css?v1.1">
<link rel="stylesheet" href="https://unpkg.com/tabulator-tables@5.6.1/dist/css/tabulator.min.css">
<link rel="stylesheet" href="/css/web/categoryTree.css?v1.1">
</th:block>
<th:block layout:fragment="layout_top_script">
<script src="/js/web/jquery.twbsPagination.js" type="text/javascript"></script>
<script>
let menuClass = "[[${param.menuClass}]]"==""?"":"[[${param.menuClass}]]";
let categoryDivCd = "[[${param.categoryDivCd}]]"==""?"":"[[${param.categoryDivCd}]]";
categoryDivCd = "[[${categoryDivCd}]]"==""?"":"[[${categoryDivCd}]]";
let menuClass = "[[${param.menuClass}]]" == "" ? "" : "[[${param.menuClass}]]";
let categoryDivCd = "[[${param.categoryDivCd}]]" == "" ? "" : "[[${param.categoryDivCd}]]";
categoryDivCd = "[[${categoryDivCd}]]" == "" ? "" : "[[${categoryDivCd}]]";
let selectUseYn = "[[${selectUseYn}]]"==""?"N":"[[${selectUseYn}]]";
let insertUseYn = "[[${insertUseYn}]]"==""?"N":"[[${insertUseYn}]]";
let updateUseYn = "[[${updateUseYn}]]"==""?"N":"[[${updateUseYn}]]";
let deleteUseYn = "[[${deleteUseYn}]]"==""?"N":"[[${deleteUseYn}]]";
let downloadUseYn = "[[${downloadUseYn}]]"==""?"N":"[[${downloadUseYn}]]";
let selectUseYn = "[[${selectUseYn}]]" == "" ? "N" : "[[${selectUseYn}]]";
let insertUseYn = "[[${insertUseYn}]]" == "" ? "N" : "[[${insertUseYn}]]";
let updateUseYn = "[[${updateUseYn}]]" == "" ? "N" : "[[${updateUseYn}]]";
let deleteUseYn = "[[${deleteUseYn}]]" == "" ? "N" : "[[${deleteUseYn}]]";
let downloadUseYn = "[[${downloadUseYn}]]" == "" ? "N" : "[[${downloadUseYn}]]";
/* 검색 관련 변수 */
let categoryNm = "[[${param.categoryName}]]"==""?"":"[[${param.categoryName}]]";
let webCategorySort = "[[${param.webCategorySort}]]";
let webCategoryDir = "[[${param.webCategoryDir}]]";
let webCategoryStart = "[[${param.webCategoryStart}]]"==""?0:"[[${param.webCategoryStart}]]";
let webCategoryLimit = "[[${param.webCategoryLimit}]]"==""?500:"[[${param.webCategoryLimit}]]";
let webCategorySearchStartDate = "[[${param.webCategorySearchStartDate}]]";
let webCategorySearchEndDate = "[[${param.webCategorySearchEndDate}]]";
let webCategorySearchDateType = "[[${param.webCategorySearchDateType}]]"==""?"A":"[[${param.webCategorySearchDateType}]]";
let categoryNm = "[[${param.categoryName}]]" == "" ? "" : "[[${param.categoryName}]]";
</script>
</th:block>
<th:block layout:fragment="layout_content">
@@ -41,11 +32,54 @@
<div class="filter_box">
<div class="form_box">
<!-- 이름input -->
<div class="search_list">
<div class="search_box">
<select id="searchCategoryDivCd" required>
<option value="">전체</option>
<!-- 검색 영역 삭제됨 -->
<div class="search_list" style="display:none;"></div>
<!-- Right Btn Box Removed as buttons moved to Tree Header -->
</div>
</div>
<!-- 트리 + 상세 레이아웃 -->
<div class="category-tree-layout">
<!-- 좌측: 트리 영역 -->
<div class="category-tree-panel">
<div class="tree-panel-header">
<span class="tree-panel-title">카테고리 목록</span>
<div class="tree-panel-actions">
<button id="btnInsertWebCategory" class="put_btn">
<img src="/image/web/notice_btn_icon.svg" alt="추가">추가
</button>
<button id="btnDeleteWebCategory" class="delete_btn">
<img src="/image/web/delete_btn_icon.svg" alt="삭제">삭제
</button>
</div>
</div>
<!-- Sub Header for Expand/Collapse -->
<div class="tree-sub-header">
<button id="btnExpandAll" class="tree-action-btn" title="모두 펼치기">▼ 펼치기</button>
<button id="btnCollapseAll" class="tree-action-btn" title="모두 접기">▶ 접기</button>
</div>
<div id="categoryTreeContainer" class="category-tree-container"></div>
</div>
<!-- 우측: 상세/편집 영역 -->
<div class="category-detail-panel">
<div class="detail-panel-header">
<span class="detail-panel-title">카테고리 정보</span>
</div>
<div id="categoryDetailContent" class="detail-panel-content">
<div class="detail-empty-state">
<p>좌측 목록에서 카테고리를 선택하세요.</p>
</div>
</div>
<!-- 등록 폼 (숨김 상태) -->
<div id="categoryInsertForm" class="detail-panel-content" style="display:none;">
<div class="detail-form-group">
<label>카테고리 구분</label>
<select id="insertCategoryDivCd" class="detail-form-select">
<option value="">선택</option>
<option value="01">다이어트 시술</option>
<option value="02">다이어트 이벤트</option>
<option value="03">쁘띠 시술</option>
@@ -54,38 +88,50 @@
<option value="06">쁘띠 전후사진</option>
</select>
</div>
<div class="search_box">
<input type="text" id="searchCategoryNm" required placeholder="카테고리명">
</div>
<button id="btnSearchWebCategory" class="search_btn" data-toggle="modal" data-target=".work_closed_modal" style="transition: all 0.2s ease-in-out 0s;">조회</button>
</div>
<div class="right_btn_box">
<button id="btnInsertWebCategory" class="put_btn">
<img src="/image/web/notice_btn_icon.svg" alt="등록">등록
</button>
<button id="btnDeleteWebCategory" class="delete_btn">
<img src="/image/web/delete_btn_icon.svg" alt="삭제">삭제
</button>
<div class="detail-form-group">
<label>카테고리명</label>
<input type="text" id="insertCategoryNm" class="detail-form-input" placeholder="카테고리명을 입력하세요">
</div>
<div class="detail-form-actions">
<button id="btnSaveCategory" class="detail-save-btn">저장</button>
<button id="btnCancelInsert" class="detail-cancel-btn">취소</button>
</div>
</div>
<div id="webCategoryGrid" class="table_box ag-theme-balham"></div>
<!-- 페이지게이션 -->
<div class="page_box">
<nav aria-label="Page navigation" class="navigation">
<ul class="pagination" id="webCategoryPagination"></ul>
</nav>
<!-- 수정 폼 (숨김 상태) -->
<div id="categoryEditForm" class="detail-panel-content" style="display:none;">
<div class="detail-form-group">
<label>카테고리 구분</label>
<input type="text" id="editCategoryDivNm" class="detail-form-input" readonly>
<input type="hidden" id="editCategoryNo">
<input type="hidden" id="editCategoryDivCd">
</div>
<div class="detail-form-group">
<label>카테고리명</label>
<input type="text" id="editCategoryNm" class="detail-form-input" placeholder="카테고리명을 입력하세요">
</div>
<div class="detail-form-group">
<label>정렬 순서</label>
<input type="number" id="editOrderNo" class="detail-form-input" placeholder="정렬 순서를 입력하세요">
</div>
<div class="detail-form-group">
<label>등록일</label>
<input type="text" id="editRegDate" class="detail-form-input" readonly>
</div>
<div class="detail-form-actions">
<button id="btnUpdateCategory" class="detail-save-btn">수정</button>
<button id="btnCancelEdit" class="detail-cancel-btn">취소</button>
</div>
</div>
</div>
</div>
</div>
<form id="webCategorySelectListForm" method="POST" target="_blank"></form>
</th:block>
<th:block layout:fragment="layout_popup">
</th:block>
<th:block layout:fragment="layout_script">
<script src="/js/web/ag-grid-community-29.3.5.min.js"></script>
<script src="/js/web/categoryManagement/CategoryManagement.js"></script>
<script src="https://unpkg.com/tabulator-tables@5.6.1/dist/js/tabulator.min.js"></script>
<script src="/js/web/categoryManagement/CategoryManagement.js?v2.0"></script>
</th:block>
</html>