From de4dc5a160678ad469c3d237255323c5765d32c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B1=EC=98=81?= Date: Fri, 22 Aug 2025 12:34:43 +0900 Subject: [PATCH] =?UTF-8?q?left=20menu=20->=20top=20menu=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=EC=99=84=EB=A3=8C=20jsp,=20css,=20menu-path.js=20?= =?UTF-8?q?=EB=AA=A8=EB=91=90=20=EB=8F=99=EC=8B=9C=EC=97=90=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=B4=EC=95=BC=20=EC=99=84=EB=A3=8C=EB=90=A8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .aiassistant/rules/xit-rules.md | 113 +++++++++ .../WEB-INF/views/layouts/base/default.jsp | 23 +- .../views/layouts/base/main_header.jsp | 220 ++++++++---------- src/main/webapp/WEB-INF/views/ma30/list.jsp | 10 +- src/main/webapp/resources/xit/menu-path.js | 16 +- src/main/webapp/resources/xit/xit-common.css | 201 ++++++++++++++++ 6 files changed, 433 insertions(+), 150 deletions(-) create mode 100644 .aiassistant/rules/xit-rules.md diff --git a/.aiassistant/rules/xit-rules.md b/.aiassistant/rules/xit-rules.md new file mode 100644 index 0000000..70c87fd --- /dev/null +++ b/.aiassistant/rules/xit-rules.md @@ -0,0 +1,113 @@ +--- +적용: 항상 +--- + +# Project Guidelines + +## 기본 중요 가이드 +### 한글로 대화, 한글 필수 +### 테스트소스는 작성 불가!! +### 중요로직 주석 필수 (한글) +### 각 컨트롤러 클래스에 스웨거 적용 +### 기존소스의 공백제거 등 로직에 필요없는 부분은 수정 불가!! +### mvc 패턴, springboot + mybatis + jsp 구조, tiles 사용 +### session 보안 +### 기본적인 코딩 스타일은 noticeSample 참조 (xml, java, jsp) +### url 은 기본적으로 사용 +### 정적 리소스는 src/main/webapp/resources/css,js,xit 등등 해당 위치에 있음. +- 모달은 src/main/webapp/WEB-INF/views/system/loginLog/list.jsp :: 로그인로그 목록 조회 페이지 참조 +- 팝업은 src/main/webapp/WEB-INF/views/system/user/auth_popup.jsp :: 팝업 호출 참조 +- 모달과 메인윈도우는 src/main/webapp/resources/xit/xit-common.css 에 css 추가 +- 팝업은 src/main/webapp/resources/xit/xit-popup.css 에 css 추가 +### 신규 sql, DDL 생성시 DB-DDL/maria/dictionary/column_word_dictionary.md, 컬럼 단어사전 무조건 참조!! +### 단어사전에 없다면 단어사전 신규 추가!! (규칙은 무조건 지켜야함 특히, 일시->dttm, 같은 경우) +### 시간이란 명칭의 컬럼도 LocalDateTime 일경우 일시(dttm) 으로 변경하여 저장 +### mybatis 기본적으로 camelCase 적용되어 있어 컬럼의 알리아스(별시) 별도로 사용하지 않아도 돼 +### DB 구조는 DB-DDL/maria/ddl/xitframework/*.sql 참조 +### 기본적인 key 는 시컨스를 이용, 총 20자리, 데이터약어 4자리, 시컨스포함 16자리 +```mariadb +SELECT CONCAT('BBSN', LPAD(NEXTVAL(seq_notice_id), 16, '0')) +``` +### paging 처리 시 controller 에서 1.totalCount 구하기, 2.setTotalCount, 3.setPagingYn 순서 중요!! 해당순서를 지켜야지만 에러 발생안함. +```java + // 1. 총 개수 조회 +int totalCount = excelSampleService.selectExcelSampleListTotalCount(paramVO); +// 2. 응답 데이터 구성 + paramVO.setTotalCount(totalCount); +// 3. 페이징 처리 + paramVO.setPagingYn("Y"); +``` + +## 2 주요 기술 및 라이브러리 +### 2.1 핵심 기술 스택 +| 기술 | 버전 | 설명 | +|------|------|------| +| Java | 개발: 1.8, 배포: 1.8 | 자바 개발 및 실행 환경 | +| Spring Boot | 2.7.18 | 스프링 기반 애플리케이션 개발 프레임워크 | +| 전자정부 프레임워크 | 4.3.0 | 한국 정부 표준 웹 개발 프레임워크 | +| Servlet | 3.1 | 웹 애플리케이션 표준 | +| Gradle | - | 빌드 및 의존성 관리 도구 | +| MariaDB | - | 관계형 데이터베이스 | + +## 2.2 주요 라이브러리 +| 라이브러리 | 버전 | 설명 | +|------------|------|------| +| MyBatis | 2.3.1 | SQL 매핑 프레임워크 | +| Apache Tiles | 3.0.8 | 레이아웃 템플릿 엔진 | +| TOAST UI Grid | 4.19.2 | 자바스크립트 그리드 라이브러리 | +| Lombok | - | 자바 코드 생성 라이브러리 | +| Apache Commons Text | 1.10.0 | 텍스트 처리 유틸리티 | +| Apache POI | 5.3.0 | 엑셀 파일 처리 라이브러리 | + +## 3. 프로젝트 구조 +### 3.1 디렉토리 구조 +``` +xit-framework/ +├── DB-DDL/ # 데이터베이스 스크립트 +│ └── maria/ # MariaDB 스크립트 +│ ├── ddl/ # 테이블 정의 스크립트 +│ └── dml/ # 샘플 데이터 스크립트 +├── src/ +│ ├── main/ +│ │ ├── java/ # 자바 소스 코드 +│ │ │ ├── egovframework/ # 전자정부 프레임워크 확장 코드 +│ │ │ └── go/kr/project/ # 프로젝트 소스 코드 +│ │ ├── resources/ # 리소스 파일 +│ │ │ ├── mybatis/ # MyBatis 설정 및 매퍼 +│ │ │ └── application.yml # 애플리케이션 설정 파일 +│ │ └── webapp/ # 웹 리소스 +│ │ ├── resources/ # 정적 리소스 (CSS, JS, 이미지 등) +│ │ └── WEB-INF/views/ # JSP 뷰 파일 +│ └── test/ # 테스트 코드 +└── build.gradle # Gradle 빌드 스크립트 +``` + +### 3.2 패키지 구조 +``` +go.kr.project/ +├── common/ # 공통 컴포넌트 +├── login/ # 로그인 관련 기능 +│ ├── controller/ # 컨트롤러 클래스 +│ ├── mapper/ # MyBatis 매퍼 인터페이스 +│ ├── model/ # 데이터 모델 클래스 +│ └── service/ # 서비스 클래스 +└── system/ # 시스템 관리 기능 + ├── auth/ # 권한 관리 + ├── code/ # 코드 관리 + ├── group/ # 그룹 관리 + ├── menu/ # 메뉴 관리 + ├── role/ # 역할 관리 + └── user/ # 사용자 관리 + ├── controller/ # 컨트롤러 클래스 + ├── mapper/ # MyBatis 매퍼 인터페이스 + ├── model/ # 데이터 모델 클래스 + └── service/ # 서비스 클래스 + +egovframework/ +├── config/ # 프레임워크 설정 +├── exception/ # 예외 처리 +├── filter/ # 필터 +├── interceptor/ # 인터셉터 +└── util/ # 유틸리티 클래스 +``` + diff --git a/src/main/webapp/WEB-INF/views/layouts/base/default.jsp b/src/main/webapp/WEB-INF/views/layouts/base/default.jsp index 5703adb..a0de862 100644 --- a/src/main/webapp/WEB-INF/views/layouts/base/default.jsp +++ b/src/main/webapp/WEB-INF/views/layouts/base/default.jsp @@ -35,9 +35,9 @@ <%----%> - + <%-- 사이드바 제거로 인해 주석처리: --%> " id="main-css"> - + <%-- 사이드바 제거로 인해 주석처리: --%> @@ -51,7 +51,7 @@ <%----%> - + <%-- 사이드바 제거로 인해 주석처리: --%> @@ -81,22 +81,15 @@ " /> - - - - - - -
+ + +
- + - + <%-- 사이드바 제거로 인해 주석처리: --%> diff --git a/src/main/webapp/WEB-INF/views/layouts/base/main_header.jsp b/src/main/webapp/WEB-INF/views/layouts/base/main_header.jsp index 7a57e04..49bbc2f 100644 --- a/src/main/webapp/WEB-INF/views/layouts/base/main_header.jsp +++ b/src/main/webapp/WEB-INF/views/layouts/base/main_header.jsp @@ -12,94 +12,18 @@ -
- - - menu - - - <%-- LEVEL1 DEPTH MENU 표출(클릭 시 하위에 있는 URL 중 첫번째 URL로 이동 가능) --%> + + + + <%-- 상단 메뉴와 서브메뉴 드롭다운 표출 --%>
@@ -117,7 +41,6 @@ <%-- 메뉴 자체의 URL을 URL 패턴으로 변환하여 추가 --%> - <%-- 메뉴 자체에 URL이 없는 경우 첫 번째 유효한 하위 URL 찾기 --%> @@ -133,7 +56,6 @@ <%-- 하위 메뉴의 URL 패턴 수집 --%> - @@ -146,32 +68,67 @@ <%-- 2. 메뉴의 URL 패턴과 현재 URL 비교 --%> - <%-- 디버깅: URL 패턴 정보 --%> - - - <%-- 디버깅: 개별 패턴 정보 --%> - - - - - - ${menu.menuNm} - + <%-- 메인 메뉴 항목 (드롭다운이 있는 경우와 없는 경우 구분) --%> +
+ + + ${menu.menuNm} + + keyboard_arrow_down + + + + <%-- 서브메뉴 드롭다운 --%> + +
+ + + + + + + <%-- 서브메뉴 활성화 상태 확인 --%> + <%-- 1. 현재 URL이 서브메뉴 URL로 시작하는 경우 --%> + + + + + <%-- 2. 서브메뉴의 URL 패턴과 현재 URL 비교 --%> + + + + + + + + + + + + + + + ${subMenu.menuNm} + + + +
+
+
@@ -226,32 +183,51 @@ $(document).ready(function() { // 세션 타이머 초기화 initSessionTimer(); - // 상단 메뉴 아이콘 초기화 (Feather 아이콘 라이브러리 사용) - if (typeof feather !== 'undefined') { - feather.replace('.top-menu-icon'); - } + // Feather 아이콘 초기화 (모든 data-feather 속성을 가진 아이콘에 적용) + initializeFeatherIcons(); + + // 상단 메뉴 드롭다운 이벤트 처리 + $('.top-menu-dropdown').hover( + function() { + // 마우스 오버 시 서브메뉴 표시 + $(this).find('.top-menu-submenu').fadeIn(200); + }, + function() { + // 마우스 아웃 시 서브메뉴 숨김 + $(this).find('.top-menu-submenu').fadeOut(200); + } + ); + - $(document).on('click', '#saveSidebarState', function () { - var saveSidebarStateValue = "sidebar-collapse"; - if( $("body").hasClass("sidebar-collapse") ){ - saveSidebarStateValue = "" + /** + * Feather 아이콘 초기화 함수 + * 모든 data-feather 속성을 가진 요소를 feather 아이콘으로 변환합니다. + */ + function initializeFeatherIcons() { + // feather 라이브러리가 로드되었는지 확인 + if (typeof feather === 'undefined') { + console.log('[DEBUG_LOG] Feather 라이브러리가 로드되지 않았습니다.'); + return; } - console.log(saveSidebarStateValue); - // AJAX 요청으로 사이드바 상태 저장 - $.ajax({ - url: "", - type: "POST", - data: {"state":saveSidebarStateValue}, - dataType: "json", - success: function(response) { - //console.log("사이드바 상태 저장 성공:", response); - }, - error: function(xhr, status, error) { - console.error(xhr.responseText); - } - }); - }); + try { + // DOM이 준비될 때까지 잠시 기다린 후 실행 + setTimeout(function() { + var $featherElements = $('[data-feather]'); + console.log('[DEBUG_LOG] 발견된 feather 아이콘 요소 수:', $featherElements.length); + + if ($featherElements.length > 0) { + // 모든 data-feather 속성을 가진 요소에 아이콘 적용 + feather.replace(); + console.log('[DEBUG_LOG] Feather 아이콘 초기화 완료'); + } else { + console.log('[DEBUG_LOG] feather 아이콘 요소를 찾을 수 없습니다.'); + } + }, 100); + } catch (error) { + console.error('[DEBUG_LOG] Feather 아이콘 초기화 오류:', error); + } + } /** * 드롭다운 메뉴 초기화 함수 diff --git a/src/main/webapp/WEB-INF/views/ma30/list.jsp b/src/main/webapp/WEB-INF/views/ma30/list.jsp index 1e9d4b6..b87cc37 100644 --- a/src/main/webapp/WEB-INF/views/ma30/list.jsp +++ b/src/main/webapp/WEB-INF/views/ma30/list.jsp @@ -81,9 +81,9 @@
  • 단속자료 목록
  • / Pages
  • @@ -172,7 +172,7 @@ var dataSource = this.createDataSource(); // 현재 선택된 perPage 값 가져오기 - var perPage = parseInt($('#perPageSelect').val() || 10, 10); + var perPage = parseInt($('#perPageSelect').val() || 15, 10); // 그리드 설정 객체 생성 var gridConfig = new XitTuiGridConfig(); @@ -180,7 +180,7 @@ // 기본 설정 gridConfig.setOptDataSource(dataSource); // 데이터소스 연결 gridConfig.setOptGridId('grid'); // 그리드를 출력할 Element ID - gridConfig.setOptGridHeight(390); // 그리드 높이(단위: px) + gridConfig.setOptGridHeight('auto'); // 그리드 높이(단위: px) gridConfig.setOptRowHeight(30); // 그리드 행 높이(단위: px) gridConfig.setOptRowHeaderType('rowNum'); // 행 첫번째 셀 타입(rowNum: 순번, checkbox: 체크박스, '': 출력 안함) gridConfig.setOptUseClientSort(false); // 서버사이드 정렬 false diff --git a/src/main/webapp/resources/xit/menu-path.js b/src/main/webapp/resources/xit/menu-path.js index a0778aa..469d914 100644 --- a/src/main/webapp/resources/xit/menu-path.js +++ b/src/main/webapp/resources/xit/menu-path.js @@ -26,19 +26,19 @@ $(function() { // 현재 URL과 일치하는 메뉴 찾기 var currentMenuPath = ""; - // 활성화된 메뉴 항목 찾기 (active 클래스가 있는 항목) - var $activeMenuItem = $('.nav.treeview.mb-4 li.nav-item > a.active'); + // 상단 메뉴에서 활성화된 메뉴 항목 찾기 (active 클래스가 있는 항목) + var $activeMenuItem = $('.top-menu-item.active'); + var $activeSubMenuItem = $('.submenu-item.active'); if ($activeMenuItem.length > 0) { - // 활성화된 메뉴가 있는 경우 - var menuText = $activeMenuItem.text().trim(); + // 활성화된 상위 메뉴가 있는 경우 + var menuText = $activeMenuItem.find('span').text().trim(); currentMenuPath = menuText; - // 하위 메뉴 확인 - var $activeSubMenuItem = $activeMenuItem.parent('li').find('.nav > li.show'); + // 활성화된 하위 메뉴가 있는지 확인 if ($activeSubMenuItem.length > 0) { - var subMenuText = $activeSubMenuItem.find('a').text().trim(); - currentMenuPath = menuText + " - " + subMenuText; + var subMenuText = $activeSubMenuItem.text().trim(); + currentMenuPath = menuText + " > " + subMenuText; } } diff --git a/src/main/webapp/resources/xit/xit-common.css b/src/main/webapp/resources/xit/xit-common.css index 34b6868..bd395d9 100644 --- a/src/main/webapp/resources/xit/xit-common.css +++ b/src/main/webapp/resources/xit/xit-common.css @@ -1,5 +1,206 @@ @charset "utf-8"; +/* ===== 상단 메뉴 레이아웃 스타일 ===== */ +/* 메인 컨텐츠가 전체 너비를 사용하도록 설정 */ +.main-content-full { + width: 100%; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* 메인 헤더 스타일 */ +.main-header { + display: flex; + align-items: center; + padding: 0 20px; + background-color: #fff; + border-bottom: 1px solid #e9ecef; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + height: 70px; + width: 100%; +} + +/* 헤더 로고 스타일 */ +.header-logo { + /*margin-right: 40px;*/ +} + +.header-logo .logo { + display: flex; + align-items: center; + text-decoration: none; + color: #495057; +} + +.header-logo .logo img { + width: auto !important; /* style.min.css의 width: 40px 비활성화 */ + height: 40px; + margin-right: 10px; +} + +.header-logo .logo-text { + font-size: 18px; + font-weight: bold; + line-height: 1.2; + color: #007bff; +} + +/* 상단 메뉴 드롭다운 스타일 */ +.top-menu-dropdown { + position: relative; + display: inline-block; +} + +.top-menu-dropdown .dropdown-arrow { + font-size: 16px; + margin-left: 5px; + transition: transform 0.2s; +} + +.top-menu-dropdown:hover .dropdown-arrow { + transform: rotate(180deg); +} + +.top-menu-submenu { + display: none; + position: absolute; + top: 100%; + left: 0; + min-width: 200px; + background-color: #fff; + border: 1px solid #e9ecef; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + border-radius: 4px; + z-index: 1000; + padding: 8px 0; +} + +.top-menu-submenu .submenu-item { + display: block; + padding: 10px 20px; + color: #495057; + text-decoration: none; + font-size: 14px; + transition: background-color 0.2s; +} + +.top-menu-submenu .submenu-item:hover { + background-color: #f8f9fa; + color: #007bff; + text-decoration: none; +} + +.top-menu-submenu .submenu-item.active { + background-color: #e3f2fd; + color: #1976d2; + font-weight: 550; +} + +.top-menu-submenu .submenu-item.active:hover { + background-color: #bbdefb; + color: #1565c0; +} + +/* 상단 메뉴 컨테이너 스타일 */ +.top-level-menu { + display: flex; + align-items: center; + margin-left: 15px; +} + +/* 메뉴 간 구분자 스타일 */ +.top-menu-dropdown:not(:last-child)::after { + content: '|'; + position: absolute; + right: -1px; + top: 50%; + transform: translateY(-50%); + color: #dee2e6; + font-size: 14px; + font-weight: normal; + z-index: 1; +} + +/* 상단 메뉴 아이콘 스타일 */ +.top-menu-icon { + width: 16px; + height: 16px; + margin-right: 5px; + display: inline-block; + vertical-align: middle; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; + fill: none; + color: inherit; +} + +/* 기존 상단 메뉴 스타일 확장 */ +.top-level-menu .top-menu-item { + display: flex; + align-items: center; + padding: 0 20px; + color: #495057; + font-weight: 500; + text-decoration: none; + height: 40px; + position: relative; + transition: color 0.2s; + font-size: 16px; +} + +.top-level-menu .top-menu-item:hover { + color: #007bff; + text-decoration: none; +} + +.top-level-menu .top-menu-item.active { + color: #007bff; + font-weight: 550; +} + +.top-menu-item.active:before { + content: ''; + position: absolute; + bottom: -15px; + left: 10px; + right: 10px; + height: 2px; + background-color: #007bff; +} + +/* 메인 컨텐츠 영역 스타일 */ +.main-content-full .main { + flex: 1; + padding: 20px; + background-color: #f8f9fa; +} + +/* 반응형 스타일 */ +@media (max-width: 992px) { + .header-logo { + margin-right: 20px; + } + + .header-logo .logo-text { + font-size: 14px; + } + + .top-level-menu .top-menu-item span { + display: none; + } + + .top-level-menu .top-menu-item { + padding: 0 12px; + } + + .top-menu-submenu { + min-width: 150px; + } +} + /* 버튼 그룹 스타일 */ .btn-group { padding-top: 10px;