kccf/.claude/skills/kccf-jsp-view/SKILL.md
ythong 80b5ed1da4 Initial commit - KCCF 문화원연합회 누리집
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 11:38:15 +09:00

218 lines
7.2 KiB
Markdown

---
name: kccf-jsp-view
description: "KCCF 시스템의 JSP 화면을 작성하거나 수정한다. 목록/등록/상세/팝업 화면, JSP 레이아웃, Ajax 연동 작업 시 사용한다. '화면 수정', 'JSP 추가', '목록화면', '등록화면', '팝업' 관련 요청에 적용."
---
# KCCF JSP 화면 작성 스킬
## 화면 위치 규칙
```
WEB-INF/jsp/
mng/{module}/ ← 관리자(인증 필요) 화면
list{Module}.jsp ← 목록
{module}Regist.jsp ← 등록/수정
select{Module}Popup.jsp ← 선택 팝업
kccf/{module}/ ← 공개 화면
list{Module}Find.jsp ← 조회/검색
popup/ ← 공개 팝업 (주소, 지도 등)
```
## 목록 화면 템플릿
```jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<c:set var="contextPath" value="${pageContext.request.contextPath}"/>
<!-- 검색 영역 -->
<form id="searchForm" name="searchForm" method="post">
<input type="hidden" name="selMenuId" value="${searchVO.selMenuId}">
<input type="hidden" name="pageNo" value="1">
<!-- 검색 조건 필드들 -->
<select name="searchAreaCode">
<option value="">전체</option>
<c:forEach var="item" items="${areaCodeList}">
<option value="${item.code}" <c:if test="${searchVO.searchAreaCode eq item.code}">selected</c:if>>
<c:out value="${item.codeNm}"/>
</option>
</c:forEach>
</select>
<input type="text" name="searchNm" value="<c:out value='${searchVO.searchNm}'/>">
<button type="button" onclick="fnSearch(1)">검색</button>
</form>
<!-- 버튼 영역 -->
<div class="btn-area">
<c:if test="${searchVO.canWrite}">
<button type="button" onclick="fnGoDetail('')">등록</button>
</c:if>
</div>
<!-- 목록 테이블 -->
<table>
<thead>
<tr>
<th>번호</th><th>명칭</th><th>등록일</th>
</tr>
</thead>
<tbody id="listBody">
<tr><td colspan="3">조회 중...</td></tr>
</tbody>
</table>
<div id="pagingDiv"></div>
<script>
var contextPath = "${contextPath}";
var selMenuId = "${searchVO.selMenuId}";
$(document).ready(function() { fnSearch(1); });
function fnSearch(pageNo) {
$("#pageNo").val(pageNo);
$.ajax({
url: contextPath + "/sec/{module}/select{Module}ListAjax.do",
type: "POST",
data: $("#searchForm").serialize(),
dataType: "json",
success: function(result) {
var list = result.data;
var sVO = result.searchVO;
var html = "";
if (!list || list.length === 0) {
html = "<tr><td colspan='3'>조회된 데이터가 없습니다.</td></tr>";
} else {
$.each(list, function(i, item) {
html += "<tr>";
html += "<td>" + item.rn + "</td>";
html += "<td><a href='javascript:fnGoDetail(\"" + item.{module}Id + "\")'>" + item.{module}Nm + "</a></td>";
html += "<td>" + (item.regDttm || "") + "</td>";
html += "</tr>";
});
}
$("#listBody").html(html);
// 페이징 처리 (기존 paging 유틸 사용)
},
error: function() { alert("목록 조회 중 오류가 발생했습니다."); }
});
}
function fnGoDetail(id) {
var params = $("#searchForm").serialize();
params += "&{module}Id=" + id;
location.href = contextPath + "/sec/{module}/{module}DetailPage.do?" + params;
}
</script>
```
## 등록/수정 화면 템플릿
```jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:set var="contextPath" value="${pageContext.request.contextPath}"/>
<form id="dataForm" name="dataForm" method="post"
action="${contextPath}/sec/{module}/insert{Module}.do">
<!-- 식별자 및 검색 조건 유지 -->
<input type="hidden" name="{module}Id" value="<c:out value='${{module}VO.{module}Id}'/>">
<input type="hidden" name="selMenuId" value="${searchVO.selMenuId}">
<input type="hidden" name="pageNo" value="${searchVO.pageNo}">
<input type="hidden" name="searchAreaCode" value="${searchVO.searchAreaCode}">
<!-- 입력 필드 -->
<table>
<tr>
<th>명칭 <span class="required">*</span></th>
<td>
<input type="text" name="{module}Nm"
value="<c:out value='${{module}VO.{module}Nm}'/>" required>
</td>
</tr>
<tr>
<th>지역</th>
<td>
<select name="areaCode">
<option value="">선택</option>
<c:forEach var="item" items="${areaCodeList}">
<option value="${item.code}"
<c:if test="${{module}VO.areaCode eq item.code}">selected</c:if>>
<c:out value="${item.codeNm}"/>
</option>
</c:forEach>
</select>
</td>
</tr>
</table>
<!-- 버튼 -->
<div class="btn-area">
<c:if test="${searchVO.canWrite}">
<button type="button" onclick="fnSave()">저장</button>
<c:if test="${not empty {module}VO.{module}Id}">
<button type="button" onclick="fnDelete()">삭제</button>
</c:if>
</c:if>
<button type="button" onclick="history.back()">목록</button>
</div>
</form>
<script>
var contextPath = "${contextPath}";
function fnSave() {
if (!$("#dataForm [name='{module}Nm']").val().trim()) {
alert("명칭을 입력하세요."); return;
}
if (confirm("저장하시겠습니까?")) {
$("#dataForm").submit();
}
}
function fnDelete() {
if (confirm("삭제하시겠습니까?")) {
$("#dataForm").attr("action", contextPath + "/sec/{module}/delete{Module}.do").submit();
}
}
</script>
```
## 팝업 화면 패턴
```jsp
<!-- 선택 팝업 (부모 창에 값 전달) -->
<script>
function fnSelect(id, nm) {
var opener = window.opener;
opener.document.getElementById("targetId").value = id;
opener.document.getElementById("targetNm").value = nm;
window.close();
}
</script>
```
## 주소 검색 팝업 호출
```javascript
function fnJusoPopup() {
var url = contextPath + "/juso/jusoPopup.do";
window.open(url, "jusoPopup", "width=570,height=420,scrollbars=yes");
}
// 주소팝업 콜백: fn_jusoPopup(roadAddr, zipCode, jibunAddr)
```
## 주의사항
- XSS 방지: 출력은 반드시 `<c:out value="${...}"/>` 또는 `fn:escapeXml()` 사용
- `${}` 직접 HTML 출력 금지 (스크립트 컨텍스트 제외)
- 신규 화면 작성 전 기존 유사 화면을 먼저 Read하여 스타일 클래스 통일
- 참조 화면: `WEB-INF/jsp/mng/bsns/` 디렉토리
## 참조
- 주소팝업: `WEB-INF/jsp/kccf/lcltt/popup/jusoPopup.jsp`
- 지도팝업: `WEB-INF/jsp/kccf/lcltt/popup/mapPopup.jsp`
- Act 선택팝업: `WEB-INF/jsp/mng/act/selectActListPopup.jsp`
- 사업 선택팝업: `WEB-INF/jsp/mng/act/selectBsnsPopup.jsp`