카테고리 관리 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 SELECT CATEGORY_NO
, CATEGORY_DIV_CD , CATEGORY_DIV_CD
, CATEGORY_NM , CATEGORY_NM
, ORDER_NO
, DATE_FORMAT(REG_DATE, '%Y-%m-%d %H:%i') AS REG_DATE , DATE_FORMAT(REG_DATE, '%Y-%m-%d %H:%i') AS REG_DATE
FROM madeu.HP_CATEGORY FROM madeu.HP_CATEGORY
<where> <where>
@@ -64,14 +65,18 @@
</select> </select>
<insert id="putCategoryManagement" parameterType="hashmap"> <insert id="putCategoryManagement" parameterType="hashmap">
<selectKey resultType="string" keyProperty="categoryNo" order="BEFORE"> <selectKey resultType="hashmap" keyProperty="categoryNo,orderNo" order="BEFORE">
SELECT NVL(MAX(CATEGORY_NO),0) + 1 FROM HP_CATEGORY WHERE CATEGORY_DIV_CD = #{categoryDivCd} 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> </selectKey>
/** CategoryManagementSql.putCategoryManagement **/ /** CategoryManagementSql.putCategoryManagement **/
INSERT INTO HP_CATEGORY( INSERT INTO HP_CATEGORY(
CATEGORY_NO CATEGORY_NO
, CATEGORY_DIV_CD , CATEGORY_DIV_CD
, CATEGORY_NM , CATEGORY_NM
, ORDER_NO
, USE_YN , USE_YN
, REG_ID , REG_ID
, REG_DATE , REG_DATE
@@ -81,6 +86,7 @@
#{categoryNo}, #{categoryNo},
#{categoryDivCd}, #{categoryDivCd},
#{categoryNm}, #{categoryNm},
#{orderNo},
'Y', 'Y',
#{regId}, #{regId},
NOW(), NOW(),
@@ -93,6 +99,7 @@
/** CategoryManagementSql.modCategoryManagement **/ /** CategoryManagementSql.modCategoryManagement **/
UPDATE HP_CATEGORY UPDATE HP_CATEGORY
SET CATEGORY_NM = #{categoryNm} SET CATEGORY_NM = #{categoryNm}
,ORDER_NO = #{orderNo}
,MOD_ID = #{modId} ,MOD_ID = #{modId}
,MOD_DATE = NOW() ,MOD_DATE = NOW()
WHERE CATEGORY_NO = #{categoryNo} 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; * CategoryManagement.js
let webCategoryTotalPages = 0; * Tabulator library based implementation
*/
/*aggird*/ // Global Table Instance
let webCategoryAgGridData = []; let categoryTable = null;
let categoryList = []; // Full flat list from server
/* 삭제할 항목들 */ // Category division map for labels
let delList = []; const categoryDivMap = {
"01": "다이어트 시술",
"02": "다이어트 이벤트",
"03": "쁘띠 시술",
"04": "쁘띠 이벤트",
"05": "다이어트 전후사진",
"06": "쁘띠 전후사진"
};
/* 등록 버튼 이중 클릭 방지 플래그 */ // Initialize
let isInsertBtnDisabled = false; $(function () {
fn_init();
fn_initEvent();
});
/**************************************************************************** function fn_init() {
* 카테고리 정보 리스트 조회 // Initial load
****************************************************************************/ fn_searchCategoryList();
function fn_selectListwebCategoryJson(){ }
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(); let formData = new FormData();
formData.append("menuClass", menuClass); formData.append("menuClass", menuClass);
// gridSort에 categoryNm가 들어오면 CATEGORY_NM로 변환
let sortValue = webCategorySort; // Server expects gridLimitStart/End for pagination, sending large range for All data
if (sortValue && sortValue.indexOf('categoryNm') !== -1) { formData.append("gridLimitStart", 0);
sortValue = sortValue.replace(/categoryNm/g, 'CATEGORY_NM'); formData.append("gridLimitEnd", 100000);
}
formData.append("gridSort", sortValue); // Search filters (Removed)
formData.append("gridLimitStart", webCategoryStart || 0); formData.append("searchCategoryDivCd", "");
formData.append("gridLimitEnd", webCategoryLimit || 10); formData.append("searchCategoryNm", "");
formData.append("categoryDivCd", categoryDivCd);
formData.append("categoryNm", categoryNm); // Sort
formData.append("searchCategoryDivCd", $("#searchCategoryDivCd").val()); formData.append("gridSort", "CATEGORY_DIV_CD ASC, CATEGORY_NM ASC");
formData.append("searchCategoryNm", $("#searchCategoryNm").val());
$.ajax({ $.ajax({
url: encodeURI('/categoryManagement/getCategoryManagementList.do'), url: encodeURI('/categoryManagement/getCategoryManagementList.do'),
@@ -37,513 +91,277 @@ function fn_selectListwebCategoryJson(){
processData: false, processData: false,
contentType: false, contentType: false,
type: 'POST', type: 'POST',
async: true,
success: function (data) { success: function (data) {
if('0'==data.msgCode){ if (data.msgCode === '0' || data.success === 'true') {
// 페이징 처리 categoryList = data.rows || [];
webCategoryTotalCount = data.totalCount; fn_renderTable(categoryList);
//$("#txt_noticeTotalCount").text(noticeTotalCount); fn_hideDetailPanel();
} else {
webCategoryTotalPages = Math.ceil(webCategoryTotalCount/webCategoryLimit); alert(data.msgDesc || "조회 중 오류가 발생했습니다.");
// 리스트 조회
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);
} }
}, },
error : function(xhr, status, error) { error: function () {
modalEvent.danger("조회 오류", "조회 중 오류가 발생하였습니다. 잠시후 다시시도하십시오."); alert("서버 통신 오류가 발생습니다.");
},
beforeSend:function(){
// 로딩열기
webCategoryGridOptions.api.showLoadingOverlay();
},
complete:function(){
} }
}); });
} }
/**************************************************************************** // Render Tabulator
* 검색하기 function fn_renderTable(list) {
****************************************************************************/ // Pre-process list to strip HTML tags for display if needed,
function fn_webCategorySearch(param){ // or we can use a formatter. Let's use a simple formatter.
if("A"!=param && "Y"!=selectUseYn){
modalEvent.warning("", "조회 권한이 없습니다.");
return false;
}
fn_webCategoryPaginReset(); categoryTable = new Tabulator("#categoryTreeContainer", {
data: list,
categoryDivCd = $("#searchCategoryDivCd").val(); layout: "fitColumns",
categoryNm = $("#searchCategoryNm").val(); selectableRows: false, // Updated: Only select via checkbox
groupBy: "categoryDivCd",
fn_selectListwebCategoryJson(); 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: [
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},
{ {
field: "categoryDivCd", formatter: "rowSelection",
headerName: "카테고리구분", titleFormatter: "rowSelection",
minWidth: 150, headerSort: false,
maxWidth: 200, width: 40,
cellStyle: { hozAlign: "center",
display: 'flex', headerHozAlign: "center",
alignItems: 'center', cellClick: function (e, cell) {
justifyContent: 'center' cell.getRow().toggleSelect();
},
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;
} }
}, },
{ {
title: "카테고리명",
field: "categoryNm", field: "categoryNm",
headerName: "카테고리명", headerSort: false,
minWidth: 200, formatter: function (cell) {
maxWidth: 300, // Strip HTML tags
cellStyle: { let val = cell.getValue();
display: 'flex', return val ? String(val).replace(/<[^>]*>?/gm, '') : '';
alignItems: 'center',
justifyContent: 'center'
}, },
// 신규행이 등록중이거나 입력이 덜 된 경우 수정 불가 cellClick: function (e, cell) {
editable: function(params) { // Handle Cell Click -> Edit
// 신규행: 입력(값이 없을 때)만 가능, 입력이 끝나면(둘 다 값이 있으면) 수정 불가, 등록중에도 수정 불가 const row = cell.getRow();
if (!params.data.regDate) { const item = row.getData();
if (params.data._isRegistering) return false;
// 둘 중 하나라도 값이 없으면 true(입력 가능), 둘 다 값이 있으면 false(수정 불가) // Highlight Row
if (!params.data.categoryDivCd || !params.data.categoryNm) return true; if (categoryTable) {
return false; const rows = categoryTable.getRows();
rows.forEach(r => r.getElement().classList.remove("row-active"));
row.getElement().classList.add("row-active");
} }
// 신규행이 등록중이거나 입력 중이면 다른 행은 수정 불가
let rowData = []; console.log("Name Cell Clicked:", item);
params.api.forEachNode(function(node) { fn_showEditForm(item);
rowData.push(node.data); }
}
],
// 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++){ * Detail Panel: Hide All
let gridSortModel = columnArr[i].colId; */
let gridSort = columnArr[i].sort; 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; * Detail Panel: Show Insert
webCategorySort += gridSortModel+' '+ gridSort + ','; */
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(); $("#categoryDetailContent").hide();
}, $("#categoryEditForm").hide();
onCellValueChanged: function(event) { $("#categoryInsertForm").show();
// 신규 row: regDate가 없을 때, 둘 다 값이 있어야 등록
if (!event.data.regDate) { // Reset fields
if (event.data.categoryDivCd && event.data.categoryNm) { $("#insertCategoryDivCd").val(preSelectDivCd || "");
// 등록중 플래그 설정 $("#insertCategoryNm").val("");
event.data._isRegistering = true; // Order No is auto-generated (MAX+1) on server
event.api.refreshCells({ force: true }); // 색상 즉시 반영
let data = { // Update Header
categoryDivCd: event.data.categoryDivCd, $(".detail-panel-title").text("카테고리 등록");
categoryNm: event.data.categoryNm, }
/**
* 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 menuClass: menuClass
}; };
$.ajax({ $.ajax({
url: '/categoryManagement/putCategoryManagement.do', url: '/categoryManagement/putCategoryManagement.do',
type: 'POST', type: 'POST',
data: JSON.stringify(data), data: JSON.stringify(data),
contentType: 'application/json', contentType: 'application/json',
success: function (res) { success: function (res) {
if (typeof res === 'string') { let response = (typeof res === 'string') ? JSON.parse(res) : res;
try { if (response.msgCode === '0') {
res = JSON.parse(res); alert("등록되었습니다.");
} catch (e) { fn_searchCategoryList(); // reload
modalEvent.danger('등록 오류', '서버 응답 파싱 오류');
event.data._isRegistering = false;
return;
}
}
if (res.msgCode === '0') {
modalEvent.success('등록 완료', '카테고리가 등록되었습니다.', function(){
fn_webCategoryOk();
});
} else { } else {
modalEvent.danger('등록 오류', res.msgDesc); alert(response.msgDesc);
} }
event.data._isRegistering = false;
event.api.refreshCells({ force: true }); // 색상 즉시 반영
}, },
error: function () { error: function () {
modalEvent.danger('등록 오류', '등록 중 오류가 발생하였습니다.'); alert("등록 중 오류가 발생습니다.");
event.data._isRegistering = false;
event.api.refreshCells({ force: true }); // 색상 즉시 반영
} }
}); });
} else { }
// 둘 중 하나라도 없으면 등록/수정 불가, 아무 동작 안함
/**
* Action: Update
*/
function fn_updateCategory() {
if ("Y" !== updateUseYn) {
alert("수정 권한이 없습니다.");
return; return;
} }
} else {
// 기존 row 수정: 둘 다 값이 있을 때만 수정 const no = $("#editCategoryNo").val();
if (event.data.categoryDivCd && event.data.categoryNm) { const divCd = $("#editCategoryDivCd").val();
let updatedData = { ...event.oldData, ...event.data }; const nm = $("#editCategoryNm").val();
updatedData.menuClass = menuClass; const orderNo = $("#editOrderNo").val() || 0;
if (!nm) { alert("카테고리명을 입력해주세요."); return; }
const data = {
categoryNo: no,
categoryDivCd: divCd,
categoryNm: nm,
orderNo: orderNo,
menuClass: menuClass
};
$.ajax({ $.ajax({
url: '/categoryManagement/modCategoryManagement.do', url: '/categoryManagement/modCategoryManagement.do',
type: 'POST', type: 'POST',
data: JSON.stringify(updatedData), data: JSON.stringify(data),
contentType: 'application/json', contentType: 'application/json',
success: function (res) { success: function (res) {
response = JSON.parse(res); let response = (typeof res === 'string') ? JSON.parse(res) : res;
if (response.msgCode === '0') { if (response.msgCode === '0') {
modalEvent.success('수정 완료', '수정이 성공적으로 반영되었습니다.'); alert("수정되었습니다.");
fn_selectListwebCategoryJson(); fn_searchCategoryList();
} else { } else {
modalEvent.danger('수정 오류', response.msgDesc); alert(response.msgDesc);
} }
}, },
error: function () { error: function () {
modalEvent.danger('수정 오류', '수정 중 오류가 발생하였습니다.'); alert("수정 중 오류가 발생습니다.");
} }
}); });
} else { }
// 둘 중 하나라도 없으면 수정 불가, 아무 동작 안함
/**
* Action: Delete
*/
function fn_deleteCategory() {
if ("Y" !== deleteUseYn) {
alert("삭제 권한이 없습니다.");
return; 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 if (!categoryTable) return;
let webCategoryGridDiv = document.querySelector('#webCategoryGrid');
// create the grid passing in the div to use together with the columns & data we want to use // Get Selected Data
new agGrid.Grid(webCategoryGridDiv, webCategoryGridOptions); const selectedData = categoryTable.getSelectedData();
if (selectedData.length === 0) {
/**************************************************************************** alert("삭제할 카테고리를 선택해주세요.");
* 페이지 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: ''
};
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; return;
} }
if(!delList || delList.length === 0) {
modalEvent.warning('', '삭제할 카테고리를 선택하세요.'); // Map to required format
return; const itemsToDelete = selectedData.map(item => ({
} categoryNo: item.categoryNo,
modalEvent.info('삭제', '선택한 카테고리를 삭제하시겠습니까?', function(){ categoryDivCd: item.categoryDivCd
let data = { }));
menuClass: menuClass,
delList: delList if (confirm(itemsToDelete.length + "건의 카테고리를 삭제하시겠습니까?")) {
const data = {
delList: itemsToDelete,
menuClass: menuClass
}; };
$.ajax({ $.ajax({
url: '/categoryManagement/delCategoryManagement.do', url: '/categoryManagement/delCategoryManagement.do',
data: JSON.stringify(data), data: JSON.stringify(data),
dataType: 'json', contentType: 'application/json',
contentType: 'application/json; charset=utf-8',
type: 'POST', type: 'POST',
async: true,
success: function (res) { success: function (res) {
if(res.msgCode === '0'){ let response = (typeof res === 'string') ? JSON.parse(res) : res;
modalEvent.success('삭제 완료', '카테고리가 삭제되었습니다.', function(){ if (response.msgCode === '0') {
fn_webCategoryOk(); alert("삭제되었습니다.");
}); fn_searchCategoryList();
} else { } else {
modalEvent.danger('삭제 오류', res.msgDesc); alert(response.msgDesc);
} }
}, },
error: function(xhr, status, error) { error: function () {
modalEvent.danger('삭제 오류', '삭제 중 오류가 발생하였습니다. 잠시후 다시시도하십시오.'); alert("삭제 중 오류가 발생습니다.");
} }
}); });
});
} }
}
$(function(){
// 페이지 init
fn_pageInit();
// 페이지 Category
fn_pageCategory();
});

View File

@@ -1,11 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{/web/layout/homeLayout}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{/web/layout/homeLayout}">
<th:block layout:fragment="layout_css"> <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="/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>
<th:block layout:fragment="layout_top_script"> <th:block layout:fragment="layout_top_script">
<script src="/js/web/jquery.twbsPagination.js" type="text/javascript"></script> <script src="/js/web/jquery.twbsPagination.js" type="text/javascript"></script>
@@ -22,15 +22,6 @@
/* 검색 관련 변수 */ /* 검색 관련 변수 */
let categoryNm = "[[${param.categoryName}]]" == "" ? "" : "[[${param.categoryName}]]"; 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}]]";
</script> </script>
</th:block> </th:block>
<th:block layout:fragment="layout_content"> <th:block layout:fragment="layout_content">
@@ -41,11 +32,54 @@
<div class="filter_box"> <div class="filter_box">
<div class="form_box"> <div class="form_box">
<!-- 이름input --> <!-- 검색 영역 삭제됨 -->
<div class="search_list"> <div class="search_list" style="display:none;"></div>
<div class="search_box">
<select id="searchCategoryDivCd" required> <!-- Right Btn Box Removed as buttons moved to Tree Header -->
<option value="">전체</option> </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="01">다이어트 시술</option>
<option value="02">다이어트 이벤트</option> <option value="02">다이어트 이벤트</option>
<option value="03">쁘띠 시술</option> <option value="03">쁘띠 시술</option>
@@ -54,38 +88,50 @@
<option value="06">쁘띠 전후사진</option> <option value="06">쁘띠 전후사진</option>
</select> </select>
</div> </div>
<div class="search_box"> <div class="detail-form-group">
<input type="text" id="searchCategoryNm" required placeholder="카테고리명"> <label>카테고리명</label>
</div> <input type="text" id="insertCategoryNm" class="detail-form-input" placeholder="카테고리명을 입력하세요">
<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> </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> </div>
<div id="webCategoryGrid" class="table_box ag-theme-balham"></div> <!-- 수정 폼 (숨김 상태) -->
<div id="categoryEditForm" class="detail-panel-content" style="display:none;">
<!-- 페이지게이션 --> <div class="detail-form-group">
<div class="page_box"> <label>카테고리 구분</label>
<nav aria-label="Page navigation" class="navigation"> <input type="text" id="editCategoryDivNm" class="detail-form-input" readonly>
<ul class="pagination" id="webCategoryPagination"></ul> <input type="hidden" id="editCategoryNo">
</nav> <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>
</div> </div>
<form id="webCategorySelectListForm" method="POST" target="_blank"></form>
</th:block> </th:block>
<th:block layout:fragment="layout_popup"> <th:block layout:fragment="layout_popup">
</th:block> </th:block>
<th:block layout:fragment="layout_script"> <th:block layout:fragment="layout_script">
<script src="/js/web/ag-grid-community-29.3.5.min.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"></script> <script src="/js/web/categoryManagement/CategoryManagement.js?v2.0"></script>
</th:block> </th:block>
</html> </html>