권한 신청·승인 워크플로 화면 및 2단계인증 UX 개선 (SFR-001)
관리자 메뉴에 권한 신청·승인 화면(#46)을 노출하고 신청/승인 처리 로직을 보강했으며, 2단계 인증(OTP) 등록·인증 화면의 사용성을 개선했다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
2a2ab99d10
commit
462e55bcd5
@ -7,10 +7,12 @@ import javax.servlet.http.HttpServletRequest;
|
|||||||
|
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.ModelMap;
|
import org.springframework.ui.ModelMap;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|
||||||
import egovframework.com.cmm.LoginVO;
|
import egovframework.com.cmm.LoginVO;
|
||||||
import egovframework.com.cmm.privacy.auditlog.ClientIpResolver;
|
import egovframework.com.cmm.privacy.auditlog.ClientIpResolver;
|
||||||
|
import egovframework.rte.ptl.mvc.tags.ui.pagination.PaginationInfo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 권한 신청·승인 워크플로 + 권한이력 조회·다운로드 컨트롤러
|
* 권한 신청·승인 워크플로 + 권한이력 조회·다운로드 컨트롤러
|
||||||
@ -35,7 +37,7 @@ public class AuthorWorkflowController {
|
|||||||
return (o instanceof LoginVO) ? (LoginVO) o : null;
|
return (o instanceof LoginVO) ? (LoginVO) o : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 권한 변경 신청 목록 */
|
/** 권한 변경 신청 목록 (AJAX/JSON) */
|
||||||
@RequestMapping(value = "/mnt/sec/authorRequest/list.do")
|
@RequestMapping(value = "/mnt/sec/authorRequest/list.do")
|
||||||
public String requestList(AuthorChangeRequestVO searchVO, ModelMap model) throws Exception {
|
public String requestList(AuthorChangeRequestVO searchVO, ModelMap model) throws Exception {
|
||||||
List<AuthorChangeRequestVO> list = authorWorkflowService.selectRequestList(searchVO);
|
List<AuthorChangeRequestVO> list = authorWorkflowService.selectRequestList(searchVO);
|
||||||
@ -44,6 +46,54 @@ public class AuthorWorkflowController {
|
|||||||
return JSON;
|
return JSON;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 권한 변경 신청 목록·승인·반려 화면(서버 렌더링).
|
||||||
|
* 뷰: /WEB-INF/jsp/site/mnt/sec/authorRequest/list.jsp
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "/mnt/sec/authorRequest/listView.do")
|
||||||
|
public String requestListView(@ModelAttribute("authorChangeRequestVO") AuthorChangeRequestVO searchVO,
|
||||||
|
ModelMap model) throws Exception {
|
||||||
|
PaginationInfo paginationInfo = new PaginationInfo();
|
||||||
|
paginationInfo.setCurrentPageNo(searchVO.getPageIndex());
|
||||||
|
paginationInfo.setRecordCountPerPage(searchVO.getRecordCountPerPage() < 1 ? 10 : searchVO.getRecordCountPerPage());
|
||||||
|
paginationInfo.setPageSize(10);
|
||||||
|
|
||||||
|
// selectRequestList 내부에서 firstIndex/totalRecordCount 를 세팅한다.
|
||||||
|
List<AuthorChangeRequestVO> list = authorWorkflowService.selectRequestList(searchVO);
|
||||||
|
paginationInfo.setTotalRecordCount(searchVO.getTotalRecordCount());
|
||||||
|
|
||||||
|
model.addAttribute("resultList", list);
|
||||||
|
model.addAttribute("paginationInfo", paginationInfo);
|
||||||
|
return "site/mnt/sec/authorRequest/list";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 권한 변경 신청 등록 화면(서버 렌더링).
|
||||||
|
* 뷰: /WEB-INF/jsp/site/mnt/sec/authorRequest/regist.jsp
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "/mnt/sec/authorRequest/registView.do")
|
||||||
|
public String requestRegistView(@ModelAttribute("authorChangeRequestVO") AuthorChangeRequestVO vo,
|
||||||
|
ModelMap model) throws Exception {
|
||||||
|
// 목표권한 드롭다운 목록
|
||||||
|
model.addAttribute("authorityList", authorWorkflowService.selectAuthorityList());
|
||||||
|
return "site/mnt/sec/authorRequest/regist";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 대상자 계정ID → 고유식별자(ESNTL_ID)·성명 자동조회 (AJAX, jsonView) */
|
||||||
|
@RequestMapping(value = "/mnt/sec/authorRequest/findTarget.do")
|
||||||
|
public String findTarget(@org.springframework.web.bind.annotation.RequestParam("trgetId") String trgetId,
|
||||||
|
ModelMap model) throws Exception {
|
||||||
|
egovframework.rte.psl.dataaccess.util.EgovMap m = authorWorkflowService.selectTargetByLoginId(trgetId);
|
||||||
|
if (m == null) {
|
||||||
|
model.addAttribute("result", "fail");
|
||||||
|
model.addAttribute("message", "해당 계정ID의 취급자를 찾을 수 없습니다.");
|
||||||
|
} else {
|
||||||
|
model.addAttribute("result", "success");
|
||||||
|
model.addAttribute("data", m);
|
||||||
|
}
|
||||||
|
return JSON;
|
||||||
|
}
|
||||||
|
|
||||||
/** 권한 변경 신청 등록 */
|
/** 권한 변경 신청 등록 */
|
||||||
@RequestMapping(value = "/mnt/sec/authorRequest/submit.do")
|
@RequestMapping(value = "/mnt/sec/authorRequest/submit.do")
|
||||||
public String submit(AuthorChangeRequestVO vo, HttpServletRequest request, ModelMap model) throws Exception {
|
public String submit(AuthorChangeRequestVO vo, HttpServletRequest request, ModelMap model) throws Exception {
|
||||||
|
|||||||
@ -66,4 +66,15 @@ public class AuthorWorkflowDAO extends EgovComAbstractDAO {
|
|||||||
public void insertChangeHistory(AuthorChangeHistoryVO vo) {
|
public void insertChangeHistory(AuthorChangeHistoryVO vo) {
|
||||||
insert("AuthorWorkflowDAO.insertChangeHistory", vo);
|
insert("AuthorWorkflowDAO.insertChangeHistory", vo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 목표권한 드롭다운용 권한코드 목록 */
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public List<egovframework.rte.psl.dataaccess.util.EgovMap> selectAuthorityList() {
|
||||||
|
return list("AuthorWorkflowDAO.selectAuthorityList", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 대상자 계정ID → 고유식별자·성명 자동조회 */
|
||||||
|
public egovframework.rte.psl.dataaccess.util.EgovMap selectTargetByLoginId(String trgetId) {
|
||||||
|
return (egovframework.rte.psl.dataaccess.util.EgovMap) selectByPk("AuthorWorkflowDAO.selectTargetByLoginId", trgetId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,4 +28,10 @@ public interface AuthorWorkflowService {
|
|||||||
|
|
||||||
/** 반려(J) — 반려 사유 기록 */
|
/** 반려(J) — 반려 사유 기록 */
|
||||||
void rejectRequest(long requestId, String confmId, String confmResn) throws Exception;
|
void rejectRequest(long requestId, String confmId, String confmResn) throws Exception;
|
||||||
|
|
||||||
|
/** 목표권한 드롭다운용 권한코드 목록 */
|
||||||
|
List<egovframework.rte.psl.dataaccess.util.EgovMap> selectAuthorityList() throws Exception;
|
||||||
|
|
||||||
|
/** 대상자 계정ID → 고유식별자·성명 자동조회 (없으면 null) */
|
||||||
|
egovframework.rte.psl.dataaccess.util.EgovMap selectTargetByLoginId(String trgetId) throws Exception;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -106,6 +106,19 @@ public class AuthorWorkflowServiceImpl implements AuthorWorkflowService {
|
|||||||
authorWorkflowDAO.updateRequestStatus(req);
|
authorWorkflowDAO.updateRequestStatus(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<egovframework.rte.psl.dataaccess.util.EgovMap> selectAuthorityList() throws Exception {
|
||||||
|
return authorWorkflowDAO.selectAuthorityList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public egovframework.rte.psl.dataaccess.util.EgovMap selectTargetByLoginId(String trgetId) throws Exception {
|
||||||
|
if (trgetId == null || trgetId.trim().length() == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return authorWorkflowDAO.selectTargetByLoginId(trgetId.trim());
|
||||||
|
}
|
||||||
|
|
||||||
/** 신청구분(N/C/R) → 이력 변경구분(G:부여/M:변경/R:회수) */
|
/** 신청구분(N/C/R) → 이력 변경구분(G:부여/M:변경/R:회수) */
|
||||||
private String mapChgSeCode(String requestSe) {
|
private String mapChgSeCode(String requestSe) {
|
||||||
if ("R".equals(requestSe)) {
|
if ("R".equals(requestSe)) {
|
||||||
|
|||||||
@ -10,6 +10,29 @@
|
|||||||
-->
|
-->
|
||||||
<sqlMap namespace="PrivacyAuthorWorkflow">
|
<sqlMap namespace="PrivacyAuthorWorkflow">
|
||||||
|
|
||||||
|
<typeAlias alias="egovMap" type="egovframework.rte.psl.dataaccess.util.EgovMap"/>
|
||||||
|
|
||||||
|
<!-- 목표권한 드롭다운용: 부여 가능한 권한코드 목록(시스템 의사권한 제외) -->
|
||||||
|
<select id="AuthorWorkflowDAO.selectAuthorityList" resultClass="egovMap">
|
||||||
|
SELECT AUTHOR_CODE AS authorCode,
|
||||||
|
COALESCE(NULLIF(AUTHOR_DC, ''), AUTHOR_NM, AUTHOR_CODE) AS authorNm
|
||||||
|
FROM COMTNAUTHORINFO
|
||||||
|
WHERE AUTHOR_CODE LIKE 'ROLE%'
|
||||||
|
AND AUTHOR_CODE NOT IN ('ROLE_ANONYMOUS', 'ROLE_RESTRICTED')
|
||||||
|
ORDER BY AUTHOR_CODE
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 대상자 계정ID → 고유식별자(ESNTL_ID)·성명 자동조회 (취급자 COMTNEMPLYRINFO) -->
|
||||||
|
<select id="AuthorWorkflowDAO.selectTargetByLoginId" parameterClass="java.lang.String" resultClass="egovMap">
|
||||||
|
SELECT CAST(EMPLYR_ID AS CHAR) AS trgetId,
|
||||||
|
ESNTL_ID AS trgetUniqId,
|
||||||
|
USER_NM AS userNm,
|
||||||
|
EMPLYR_STTUS_CODE AS sttusCode
|
||||||
|
FROM COMTNEMPLYRINFO
|
||||||
|
WHERE EMPLYR_ID = #value#
|
||||||
|
LIMIT 1
|
||||||
|
</select>
|
||||||
|
|
||||||
<!-- ===================== #3 신청·승인 워크플로 (AuthorWorkflowDAO) ===================== -->
|
<!-- ===================== #3 신청·승인 워크플로 (AuthorWorkflowDAO) ===================== -->
|
||||||
|
|
||||||
<insert id="AuthorWorkflowDAO.insertRequest"
|
<insert id="AuthorWorkflowDAO.insertRequest"
|
||||||
|
|||||||
@ -9,6 +9,30 @@
|
|||||||
-->
|
-->
|
||||||
<sqlMap namespace="PrivacyAuthorWorkflow">
|
<sqlMap namespace="PrivacyAuthorWorkflow">
|
||||||
|
|
||||||
|
<typeAlias alias="egovMap" type="egovframework.rte.psl.dataaccess.util.EgovMap"/>
|
||||||
|
|
||||||
|
<!-- 목표권한 드롭다운용: 부여 가능한 권한코드 목록(시스템 의사권한 제외) -->
|
||||||
|
<select id="AuthorWorkflowDAO.selectAuthorityList" resultClass="egovMap">
|
||||||
|
SELECT AUTHOR_CODE AS authorCode,
|
||||||
|
COALESCE(AUTHOR_DC, AUTHOR_NM, AUTHOR_CODE) AS authorNm
|
||||||
|
FROM COMTNAUTHORINFO
|
||||||
|
WHERE AUTHOR_CODE LIKE 'ROLE%'
|
||||||
|
AND AUTHOR_CODE NOT IN ('ROLE_ANONYMOUS', 'ROLE_RESTRICTED')
|
||||||
|
ORDER BY AUTHOR_CODE
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 대상자 계정ID → 고유식별자(ESNTL_ID)·성명 자동조회 (취급자 COMTNEMPLYRINFO) -->
|
||||||
|
<select id="AuthorWorkflowDAO.selectTargetByLoginId" parameterClass="java.lang.String" resultClass="egovMap">
|
||||||
|
SELECT trgetId, trgetUniqId, userNm, sttusCode FROM (
|
||||||
|
SELECT CAST(EMPLYR_ID AS VARCHAR2(20)) AS trgetId,
|
||||||
|
ESNTL_ID AS trgetUniqId,
|
||||||
|
USER_NM AS userNm,
|
||||||
|
EMPLYR_STTUS_CODE AS sttusCode
|
||||||
|
FROM COMTNEMPLYRINFO
|
||||||
|
WHERE EMPLYR_ID = #value#
|
||||||
|
) WHERE ROWNUM = 1
|
||||||
|
</select>
|
||||||
|
|
||||||
<insert id="AuthorWorkflowDAO.insertRequest"
|
<insert id="AuthorWorkflowDAO.insertRequest"
|
||||||
parameterClass="egovframework.com.cmm.privacy.author.AuthorChangeRequestVO">
|
parameterClass="egovframework.com.cmm.privacy.author.AuthorChangeRequestVO">
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
|
|||||||
@ -4,7 +4,9 @@
|
|||||||
<%--
|
<%--
|
||||||
2차(추가) 인증 입력 화면 — 개인정보보호 기능개선 사업 SFR-003 / #52
|
2차(추가) 인증 입력 화면 — 개인정보보호 기능개선 사업 SFR-003 / #52
|
||||||
· /uat/uia/secondFactor.do (SecondFactorInterceptor excludePatterns 대상 → 루프 방지)
|
· /uat/uia/secondFactor.do (SecondFactorInterceptor excludePatterns 대상 → 루프 방지)
|
||||||
· 자립형(관리자 프레임 진입 前 보안 게이트). mnt 레거시 톤(각진 표/블루).
|
· 자립형(관리자 프레임 진입 前 보안 게이트)이라 /mnt/include/head.do 는 부르지 않는다.
|
||||||
|
대신 인라인 스타일을 실제 mnt 관리자 스킨 토큰(board.css .write/.btn1·2/.tbox, admin.css #005ea8)으로
|
||||||
|
맞춰 형제 화면(mnt/sec/authorRequest)과 톤을 통일한다 — 각진 사각형, 그림자 없음, 파란 포인트.
|
||||||
· 인증정보 본문(secret/코드)은 화면·로그에 노출하지 않는다(SER-003).
|
· 인증정보 본문(secret/코드)은 화면·로그에 노출하지 않는다(SER-003).
|
||||||
--%>
|
--%>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@ -14,32 +16,46 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>2단계 인증 - 문화품앗이 관리자</title>
|
<title>2단계 인증 - 문화품앗이 관리자</title>
|
||||||
<style>
|
<style>
|
||||||
|
/* mnt 관리자 백오피스 스킨 토큰(board.css/admin.css) 재현 — 자립형 게이트용 인라인 */
|
||||||
* { box-sizing:border-box; }
|
* { box-sizing:border-box; }
|
||||||
body { margin:0; font-family:"맑은 고딕","Malgun Gothic",dotum,sans-serif; background:#eef1f5; color:#222; }
|
body { margin:0; font-family:"나눔고딕","NanumGothic","맑은 고딕","Malgun Gothic",dotum,sans-serif;
|
||||||
.wrap { max-width:460px; margin:8vh auto; background:#fff; border:1px solid #c7d0dc; border-top:4px solid #1f4e8c; }
|
background:#f4f4f4; color:#333; font-size:13px; }
|
||||||
.head { background:#1f4e8c; color:#fff; padding:16px 22px; font-size:18px; font-weight:bold; }
|
.gate { width:480px; margin:9vh auto; background:#fff; border:1px solid #d1cdc9; }
|
||||||
.body { padding:26px 24px; }
|
.gate-hd { border-top:3px solid #005ea8; background:#fbfbfb; padding:15px 20px;
|
||||||
.lead { font-size:13px; color:#555; line-height:1.6; margin:0 0 18px; }
|
border-bottom:1px solid #d1cdc9; }
|
||||||
.method { display:inline-block; background:#eaf1fb; color:#1f4e8c; border:1px solid #b6cbe8;
|
.gate-hd h1 { margin:0; font-size:17px; font-weight:bold; color:#005ea8; }
|
||||||
font-size:12px; font-weight:bold; padding:3px 10px; margin-bottom:14px; }
|
.gate-bd { padding:22px 20px; }
|
||||||
label { display:block; font-size:13px; font-weight:bold; margin:0 0 6px; }
|
.lead { font-size:13px; color:#555; line-height:1.7; margin:0 0 16px; }
|
||||||
input[type=text] { width:100%; height:44px; border:1px solid #98a6ba; padding:0 12px; font-size:20px;
|
/* 폼 테이블 — board.css .write 재현 */
|
||||||
letter-spacing:6px; text-align:center; }
|
table.write { width:100%; border-top:2px solid #5a5a5a; border-collapse:collapse; margin:0 0 16px; }
|
||||||
.btn { width:100%; height:46px; border:0; background:#1f4e8c; color:#fff; font-size:15px; font-weight:bold;
|
table.write th, table.write td { padding:10px; border-bottom:1px solid #d1cdc9; font-size:13px; vertical-align:middle; }
|
||||||
cursor:pointer; margin-top:16px; }
|
table.write th { width:34%; background:#f4f4f4; color:#717171; text-align:left; font-weight:bold; }
|
||||||
.btn:hover { background:#173c6e; }
|
/* 입력상자 — board.css .tbox 재현(코드 입력은 확대) */
|
||||||
.btn.sub { background:#5b6b80; }
|
input.tbox { background:#fff; border:1px solid #c6c6c6; height:34px; padding:0 10px; font-size:14px; }
|
||||||
.err { background:#fdecec; border:1px solid #e6b3b3; color:#b52626; font-size:13px; padding:10px 12px; margin:0 0 16px; }
|
input.code { width:100%; height:46px; border:1px solid #c6c6c6; font-size:22px; letter-spacing:8px;
|
||||||
.info { background:#f4f7fb; border:1px solid #d3ddea; color:#33507a; font-size:12.5px; padding:12px 14px; line-height:1.6; }
|
text-align:center; }
|
||||||
.foot { padding:14px 24px; border-top:1px solid #e3e8ef; font-size:12px; color:#8894a5; text-align:center; }
|
/* 인증방식 뱃지 */
|
||||||
a.enroll { color:#1f4e8c; font-weight:bold; text-decoration:none; }
|
.method { display:inline-block; background:#eef4fb; color:#005ea8; border:1px solid #b9d3ea;
|
||||||
a.enroll:hover { text-decoration:underline; }
|
font-size:12px; font-weight:bold; padding:3px 10px; }
|
||||||
|
/* 버튼존 — board.css .btn1/.btn2 재현 */
|
||||||
|
.btn_zone { overflow:hidden; text-align:center; margin:4px 0 0; }
|
||||||
|
.btn1, .btn2 { display:inline-block; padding:12px 22px; color:#fff !important; font-weight:bold;
|
||||||
|
text-align:center; text-decoration:none; cursor:pointer; font-size:14px; }
|
||||||
|
.btn2 { background:#149fd3; border:1px solid #0d8ebe; }
|
||||||
|
.btn1 { background:#7d7d7d; border:1px solid #6e6e6e; }
|
||||||
|
.btn_full { width:100%; }
|
||||||
|
.err { background:#fdecec; border:1px solid #e6b3b3; color:#b52626; font-size:13px; padding:10px 12px; margin:0 0 14px; }
|
||||||
|
.info { background:#f4f7fb; border:1px solid #cdddec; color:#33507a; font-size:12.5px; padding:12px 14px; line-height:1.7; margin:0 0 16px; }
|
||||||
|
.reenroll { margin:16px 0 0; font-size:12px; text-align:center; color:#777; }
|
||||||
|
.reenroll a, a.link { color:#005ea8; font-weight:bold; text-decoration:none; }
|
||||||
|
.reenroll a:hover, a.link:hover { text-decoration:underline; }
|
||||||
|
.gate-ft { border-top:1px solid #e3e3e3; padding:12px 20px; font-size:12px; color:#999; text-align:center; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
<div class="gate">
|
||||||
<div class="head">2단계 인증</div>
|
<div class="gate-hd"><h1>2단계 인증</h1></div>
|
||||||
<div class="body">
|
<div class="gate-bd">
|
||||||
|
|
||||||
<c:if test="${not empty errorMsg}">
|
<c:if test="${not empty errorMsg}">
|
||||||
<div class="err">${errorMsg}</div>
|
<div class="err">${errorMsg}</div>
|
||||||
@ -54,29 +70,21 @@
|
|||||||
OTP(일회용 비밀번호) 앱(Google Authenticator, OTP 등)을 이용해 등록할 수 있습니다.
|
OTP(일회용 비밀번호) 앱(Google Authenticator, OTP 등)을 이용해 등록할 수 있습니다.
|
||||||
등록 전까지는 취급자 화면 접근이 제한될 수 있습니다.
|
등록 전까지는 취급자 화면 접근이 제한될 수 있습니다.
|
||||||
</div>
|
</div>
|
||||||
<a href="<c:url value='/uat/uia/secondFactorEnroll.do'/>">
|
<div class="btn_zone">
|
||||||
<button type="button" class="btn">OTP 인증수단 등록하기</button>
|
<a href="<c:url value='/uat/uia/secondFactorEnroll.do'/>" class="btn2 btn_full">OTP 인증수단 등록하기</a>
|
||||||
</a>
|
</div>
|
||||||
</c:when>
|
</c:when>
|
||||||
|
|
||||||
<%-- 시도횟수 초과 --%>
|
<%-- 시도횟수 초과 --%>
|
||||||
<c:when test="${methodType eq 'LOCKED'}">
|
<c:when test="${methodType eq 'LOCKED'}">
|
||||||
<p class="lead">인증 시도 횟수를 초과했습니다.<br/>보안을 위해 다시 로그인해 주세요.</p>
|
<p class="lead">인증 시도 횟수를 초과했습니다.<br/>보안을 위해 다시 로그인해 주세요.</p>
|
||||||
<a href="<c:url value='/uat/uia/actionLogout.do'/>">
|
<div class="btn_zone">
|
||||||
<button type="button" class="btn sub">로그인 화면으로</button>
|
<a href="<c:url value='/uat/uia/actionLogout.do'/>" class="btn1 btn_full">로그인 화면으로</a>
|
||||||
</a>
|
</div>
|
||||||
</c:when>
|
</c:when>
|
||||||
|
|
||||||
<%-- 정상 코드 입력 --%>
|
<%-- 정상 코드 입력 --%>
|
||||||
<c:otherwise>
|
<c:otherwise>
|
||||||
<div class="method">
|
|
||||||
<c:choose>
|
|
||||||
<c:when test="${methodType eq 'SMS'}">휴대폰(SMS) 인증</c:when>
|
|
||||||
<c:when test="${methodType eq 'CERT'}">공동인증서(GPKI) 인증</c:when>
|
|
||||||
<c:otherwise>OTP(일회용 비밀번호) 인증</c:otherwise>
|
|
||||||
</c:choose>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<c:choose>
|
<c:choose>
|
||||||
<c:when test="${methodType eq 'SMS'}">
|
<c:when test="${methodType eq 'SMS'}">
|
||||||
<p class="lead">등록된 휴대폰
|
<p class="lead">등록된 휴대폰
|
||||||
@ -96,21 +104,45 @@
|
|||||||
|
|
||||||
<form method="post" action="<c:url value='/uat/uia/secondFactorConfirm.do'/>" autocomplete="off">
|
<form method="post" action="<c:url value='/uat/uia/secondFactorConfirm.do'/>" autocomplete="off">
|
||||||
<input type="hidden" name="returnUrl" value="${fn:escapeXml(returnUrl)}"/>
|
<input type="hidden" name="returnUrl" value="${fn:escapeXml(returnUrl)}"/>
|
||||||
<label for="authCode">인증코드</label>
|
<table summary="2단계 인증코드 입력" class="write">
|
||||||
<input type="text" id="authCode" name="authCode" maxlength="16" inputmode="numeric"
|
<caption>2단계 인증코드 입력</caption>
|
||||||
placeholder="인증코드 입력" autofocus/>
|
<colgroup><col width="34%"/><col width="*"/></colgroup>
|
||||||
<button type="submit" class="btn">인증 확인</button>
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>인증방식</th>
|
||||||
|
<td>
|
||||||
|
<span class="method">
|
||||||
|
<c:choose>
|
||||||
|
<c:when test="${methodType eq 'SMS'}">휴대폰(SMS) 인증</c:when>
|
||||||
|
<c:when test="${methodType eq 'CERT'}">공동인증서(GPKI) 인증</c:when>
|
||||||
|
<c:otherwise>OTP(일회용 비밀번호) 인증</c:otherwise>
|
||||||
|
</c:choose>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="authCode">인증코드</label></th>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="authCode" name="authCode" class="tbox code" maxlength="16"
|
||||||
|
inputmode="numeric" placeholder="인증코드 입력" autofocus/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="btn_zone">
|
||||||
|
<button type="submit" class="btn2 btn_full">인증 확인</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p style="margin:16px 0 0; font-size:12px; text-align:center;">
|
<p class="reenroll">
|
||||||
인증수단을 변경/재등록하려면
|
인증수단을 변경/재등록하려면
|
||||||
<a class="enroll" href="<c:url value='/uat/uia/secondFactorEnroll.do'/>">여기</a>를 누르세요.
|
<a href="<c:url value='/uat/uia/secondFactorEnroll.do'/>">여기</a>를 누르세요.
|
||||||
</p>
|
</p>
|
||||||
</c:otherwise>
|
</c:otherwise>
|
||||||
</c:choose>
|
</c:choose>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="foot">한국문화예술위원회 문화품앗이 · 개인정보취급자 보호</div>
|
<div class="gate-ft">한국문화예술위원회 문화품앗이 · 개인정보취급자 보호</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
· 비밀키(secret)는 등록 1회 표기용이며 저장·로깅하지 않는다. QR 스캔 등록 + 수동 입력(보조) 병행.
|
· 비밀키(secret)는 등록 1회 표기용이며 저장·로깅하지 않는다. QR 스캔 등록 + 수동 입력(보조) 병행.
|
||||||
· QR(otpQrDataUri)은 서버측 ZXing 으로 생성한 자립형 data URI PNG — 외부 리소스/네트워크 없음(폐쇄망).
|
· QR(otpQrDataUri)은 서버측 ZXing 으로 생성한 자립형 data URI PNG — 외부 리소스/네트워크 없음(폐쇄망).
|
||||||
· 저장소(DaoOtpSecretStore)는 OTP_DEVICE 테이블에 비밀키 암호문을 영구 저장 — 재기동에도 유지.
|
· 저장소(DaoOtpSecretStore)는 OTP_DEVICE 테이블에 비밀키 암호문을 영구 저장 — 재기동에도 유지.
|
||||||
|
· 자립형 게이트라 head.do 미사용, 인라인 스타일을 mnt 관리자 스킨 토큰(board.css/admin.css)으로 통일.
|
||||||
--%>
|
--%>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ko">
|
<html lang="ko">
|
||||||
@ -15,37 +16,48 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>OTP 인증수단 등록 - 문화품앗이 관리자</title>
|
<title>OTP 인증수단 등록 - 문화품앗이 관리자</title>
|
||||||
<style>
|
<style>
|
||||||
|
/* mnt 관리자 백오피스 스킨 토큰(board.css/admin.css) 재현 — 자립형 게이트용 인라인 */
|
||||||
* { box-sizing:border-box; }
|
* { box-sizing:border-box; }
|
||||||
body { margin:0; font-family:"맑은 고딕","Malgun Gothic",dotum,sans-serif; background:#eef1f5; color:#222; }
|
body { margin:0; font-family:"나눔고딕","NanumGothic","맑은 고딕","Malgun Gothic",dotum,sans-serif;
|
||||||
.wrap { max-width:520px; margin:6vh auto; background:#fff; border:1px solid #c7d0dc; border-top:4px solid #1f4e8c; }
|
background:#f4f4f4; color:#333; font-size:13px; }
|
||||||
.head { background:#1f4e8c; color:#fff; padding:16px 22px; font-size:18px; font-weight:bold; }
|
.gate { width:540px; margin:6vh auto; background:#fff; border:1px solid #d1cdc9; }
|
||||||
.body { padding:24px; }
|
.gate-hd { border-top:3px solid #005ea8; background:#fbfbfb; padding:15px 20px;
|
||||||
|
border-bottom:1px solid #d1cdc9; }
|
||||||
|
.gate-hd h1 { margin:0; font-size:17px; font-weight:bold; color:#005ea8; }
|
||||||
|
.gate-bd { padding:22px 20px; }
|
||||||
ol.steps { margin:0 0 18px; padding-left:20px; font-size:13px; color:#444; line-height:1.9; }
|
ol.steps { margin:0 0 18px; padding-left:20px; font-size:13px; color:#444; line-height:1.9; }
|
||||||
.qrbox { text-align:center; margin:0 0 14px; }
|
/* 소제목 바 — mnt 각진 톤 */
|
||||||
.qrbox img { width:220px; height:220px; border:1px solid #c7d0dc; background:#fff; padding:8px; }
|
.sub-h { border-left:3px solid #005ea8; background:#f4f4f4; padding:7px 12px; font-size:13px;
|
||||||
.qrcap { font-size:12px; color:#556; margin:8px 0 0; }
|
font-weight:bold; color:#444; margin:0 0 12px; }
|
||||||
.divider { display:flex; align-items:center; text-align:center; color:#8894a5; font-size:12px; margin:14px 0 6px; }
|
.qrbox { text-align:center; margin:0 0 16px; }
|
||||||
.divider:before, .divider:after { content:""; flex:1; border-bottom:1px solid #dfe5ee; }
|
.qrbox img { width:220px; height:220px; border:1px solid #d1cdc9; background:#fff; padding:8px; }
|
||||||
.divider span { padding:0 10px; }
|
.qrcap { font-size:12px; color:#717171; margin:8px 0 0; }
|
||||||
.secret { background:#f4f7fb; border:1px dashed #98a6ba; padding:14px; text-align:center; margin:0 0 6px; }
|
.secret { background:#f4f4f4; border:1px solid #d1cdc9; padding:14px; text-align:center; margin:0 0 6px; }
|
||||||
.secret code { font-size:18px; font-weight:bold; letter-spacing:2px; word-break:break-all; color:#173c6e; }
|
.secret code { font-size:18px; font-weight:bold; letter-spacing:2px; word-break:break-all; color:#005ea8; }
|
||||||
.uri { font-size:11px; color:#8894a5; word-break:break-all; margin:0 0 18px; text-align:center; }
|
.uri { font-size:11px; color:#999; word-break:break-all; margin:0 0 18px; text-align:center; }
|
||||||
label { display:block; font-size:13px; font-weight:bold; margin:14px 0 6px; }
|
/* 폼 테이블 — board.css .write 재현 */
|
||||||
input[type=text] { width:100%; height:44px; border:1px solid #98a6ba; padding:0 12px; font-size:20px;
|
table.write { width:100%; border-top:2px solid #5a5a5a; border-collapse:collapse; margin:0 0 16px; }
|
||||||
letter-spacing:6px; text-align:center; }
|
table.write th, table.write td { padding:10px; border-bottom:1px solid #d1cdc9; font-size:13px; vertical-align:middle; }
|
||||||
.btn { width:100%; height:46px; border:0; background:#1f4e8c; color:#fff; font-size:15px; font-weight:bold;
|
table.write th { width:40%; background:#f4f4f4; color:#717171; text-align:left; font-weight:bold; }
|
||||||
cursor:pointer; margin-top:16px; }
|
input.tbox { background:#fff; border:1px solid #c6c6c6; height:34px; padding:0 10px; font-size:14px; }
|
||||||
.btn:hover { background:#173c6e; }
|
input.code { width:100%; height:46px; border:1px solid #c6c6c6; font-size:22px; letter-spacing:8px;
|
||||||
.btn.sub { background:#5b6b80; }
|
text-align:center; }
|
||||||
|
/* 버튼존 — board.css .btn1/.btn2 재현 */
|
||||||
|
.btn_zone { overflow:hidden; text-align:center; margin:0 0 10px; }
|
||||||
|
.btn1, .btn2 { display:inline-block; padding:12px 22px; color:#fff !important; font-weight:bold;
|
||||||
|
text-align:center; text-decoration:none; cursor:pointer; font-size:14px; }
|
||||||
|
.btn2 { background:#149fd3; border:1px solid #0d8ebe; }
|
||||||
|
.btn1 { background:#7d7d7d; border:1px solid #6e6e6e; }
|
||||||
|
.btn_full { width:100%; }
|
||||||
.err { background:#fdecec; border:1px solid #e6b3b3; color:#b52626; font-size:13px; padding:10px 12px; margin:0 0 16px; }
|
.err { background:#fdecec; border:1px solid #e6b3b3; color:#b52626; font-size:13px; padding:10px 12px; margin:0 0 16px; }
|
||||||
.warn { background:#fff8e6; border:1px solid #e6d59b; color:#8a6d1a; font-size:12px; padding:10px 12px; margin:16px 0 0; line-height:1.6; }
|
.warn { background:#fff8e6; border:1px solid #e6d59b; color:#8a6d1a; font-size:12px; padding:10px 12px; margin:16px 0 0; line-height:1.7; }
|
||||||
.foot { padding:14px 24px; border-top:1px solid #e3e8ef; font-size:12px; color:#8894a5; text-align:center; }
|
.gate-ft { border-top:1px solid #e3e3e3; padding:12px 20px; font-size:12px; color:#999; text-align:center; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
<div class="gate">
|
||||||
<div class="head">OTP 인증수단 등록</div>
|
<div class="gate-hd"><h1>OTP 인증수단 등록</h1></div>
|
||||||
<div class="body">
|
<div class="gate-bd">
|
||||||
|
|
||||||
<c:if test="${not empty enrollError}">
|
<c:if test="${not empty enrollError}">
|
||||||
<div class="err">${enrollError}</div>
|
<div class="err">${enrollError}</div>
|
||||||
@ -67,28 +79,40 @@
|
|||||||
</c:if>
|
</c:if>
|
||||||
|
|
||||||
<c:if test="${not empty otpSecret}">
|
<c:if test="${not empty otpSecret}">
|
||||||
<div class="divider"><span>QR 스캔이 어려우면 수동 입력</span></div>
|
<p class="sub-h">QR 스캔이 어려우면 수동 입력</p>
|
||||||
<div class="secret"><code>${fn:escapeXml(otpSecret)}</code></div>
|
<div class="secret"><code>${fn:escapeXml(otpSecret)}</code></div>
|
||||||
<div class="uri">otpauth URI: ${fn:escapeXml(otpauthUri)}</div>
|
<div class="uri">otpauth URI: ${fn:escapeXml(otpauthUri)}</div>
|
||||||
</c:if>
|
</c:if>
|
||||||
|
|
||||||
<form method="post" action="<c:url value='/uat/uia/secondFactorEnrollConfirm.do'/>" autocomplete="off">
|
<form method="post" action="<c:url value='/uat/uia/secondFactorEnrollConfirm.do'/>" autocomplete="off">
|
||||||
<label for="authCode">앱 표시 6자리 코드</label>
|
<table summary="앱 표시 6자리 코드 입력" class="write">
|
||||||
<input type="text" id="authCode" name="authCode" maxlength="8" inputmode="numeric"
|
<caption>앱 표시 6자리 코드 입력</caption>
|
||||||
placeholder="6자리 코드" autofocus/>
|
<colgroup><col width="40%"/><col width="*"/></colgroup>
|
||||||
<button type="submit" class="btn">등록 완료</button>
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th><label for="authCode">앱 표시 6자리 코드</label></th>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="authCode" name="authCode" class="tbox code" maxlength="8"
|
||||||
|
inputmode="numeric" placeholder="6자리 코드" autofocus/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="btn_zone">
|
||||||
|
<button type="submit" class="btn2 btn_full">등록 완료</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<a href="<c:url value='/uat/uia/secondFactorEnroll.do'/>">
|
<div class="btn_zone">
|
||||||
<button type="button" class="btn sub">비밀키 다시 발급</button>
|
<a href="<c:url value='/uat/uia/secondFactorEnroll.do'/>" class="btn1 btn_full">비밀키 다시 발급</a>
|
||||||
</a>
|
</div>
|
||||||
|
|
||||||
<div class="warn">
|
<div class="warn">
|
||||||
비밀키는 이 화면에서만 표시됩니다(저장·재조회 불가). 등록을 완료하기 전에 창을 닫으면
|
비밀키는 이 화면에서만 표시됩니다(저장·재조회 불가). 등록을 완료하기 전에 창을 닫으면
|
||||||
처음부터 다시 발급해야 합니다.
|
처음부터 다시 발급해야 합니다.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="foot">한국문화예술위원회 문화품앗이 · 개인정보취급자 보호</div>
|
<div class="gate-ft">한국문화예술위원회 문화품앗이 · 개인정보취급자 보호</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -48,13 +48,13 @@
|
|||||||
|| param.page eq '9100022' || param.page eq '9100031' || param.page eq '9100034' || param.page eq '9100041'
|
|| param.page eq '9100022' || param.page eq '9100031' || param.page eq '9100034' || param.page eq '9100041'
|
||||||
|| param.page eq '9100042'|| param.page eq '9100043'
|
|| param.page eq '9100042'|| param.page eq '9100043'
|
||||||
|| param.page eq '9100071' || param.page eq '9100072' || param.page eq '9100080'
|
|| param.page eq '9100071' || param.page eq '9100072' || param.page eq '9100080'
|
||||||
|| param.page eq '9100091' || param.page eq '9100092' || param.page eq '9100093' || param.page eq ''}">class="on"</c:if>><a href="#none">시스템관리</a>
|
|| param.page eq '9100091' || param.page eq '9100092' || param.page eq '9100093' || param.page eq '9100035' || param.page eq ''}">class="on"</c:if>><a href="#none">시스템관리</a>
|
||||||
<!-- 2 Depth -->
|
<!-- 2 Depth -->
|
||||||
<ul <c:if test="${param.page eq '9100012'|| param.page eq '9100012' || param.page eq '9100020' || param.page eq '9100021'
|
<ul <c:if test="${param.page eq '9100012'|| param.page eq '9100012' || param.page eq '9100020' || param.page eq '9100021'
|
||||||
|| param.page eq '9100022' || param.page eq '9100031' || param.page eq '9100034'|| param.page eq '9100041'
|
|| param.page eq '9100022' || param.page eq '9100031' || param.page eq '9100034'|| param.page eq '9100041'
|
||||||
|| param.page eq '9100042'|| param.page eq '9100043'
|
|| param.page eq '9100042'|| param.page eq '9100043'
|
||||||
|| param.page eq '9100071' || param.page eq '9100072' || param.page eq '9100080'
|
|| param.page eq '9100071' || param.page eq '9100072' || param.page eq '9100080'
|
||||||
|| param.page eq '9100091' || param.page eq '9100092' || param.page eq '9100093' || param.page eq ''}">style="display: block;"</c:if>>
|
|| param.page eq '9100091' || param.page eq '9100092' || param.page eq '9100093' || param.page eq '9100035' || param.page eq ''}">style="display: block;"</c:if>>
|
||||||
<c:forEach items="${list_menulist}" var="menuUpperVo2">
|
<c:forEach items="${list_menulist}" var="menuUpperVo2">
|
||||||
<c:if test="${menuUpperVo2.menuNo eq '9100010'}">
|
<c:if test="${menuUpperVo2.menuNo eq '9100010'}">
|
||||||
<li <c:if test="${param.page eq '9100012'}">class="on"</c:if>><a href="#none">${menuUpperVo2.menuNm}</a>
|
<li <c:if test="${param.page eq '9100012'}">class="on"</c:if>><a href="#none">${menuUpperVo2.menuNm}</a>
|
||||||
@ -94,10 +94,10 @@
|
|||||||
</li>
|
</li>
|
||||||
</c:if>
|
</c:if>
|
||||||
<c:if test="${menuUpperVo2.menuNo eq '9100030'}">
|
<c:if test="${menuUpperVo2.menuNo eq '9100030'}">
|
||||||
<li <c:if test="${param.page eq '9100031' || param.page eq '9100034'}">class="on"</c:if>><a href="#none">${menuUpperVo2.menuNm}</a>
|
<li <c:if test="${param.page eq '9100031' || param.page eq '9100034' || param.page eq '9100035'}">class="on"</c:if>><a href="#none">${menuUpperVo2.menuNm}</a>
|
||||||
<c:forEach items="${list_menulist}" var="menuUpperVo3">
|
<c:forEach items="${list_menulist}" var="menuUpperVo3">
|
||||||
<c:if test="${menuUpperVo3.menuNo eq '9100030'}">
|
<c:if test="${menuUpperVo3.menuNo eq '9100030'}">
|
||||||
<ul <c:if test="${param.page eq '9100031' || param.page eq '9100034'}">style="display: block;"</c:if>>
|
<ul <c:if test="${param.page eq '9100031' || param.page eq '9100034' || param.page eq '9100035'}">style="display: block;"</c:if>>
|
||||||
<c:forEach items="${list_menulist}" var="menuUpperVo4">
|
<c:forEach items="${list_menulist}" var="menuUpperVo4">
|
||||||
<c:choose>
|
<c:choose>
|
||||||
<c:when test="${menuUpperVo4.menuNo eq '9100031'}">
|
<c:when test="${menuUpperVo4.menuNo eq '9100031'}">
|
||||||
@ -106,6 +106,9 @@
|
|||||||
<c:when test="${menuUpperVo4.menuNo eq '9100034'}">
|
<c:when test="${menuUpperVo4.menuNo eq '9100034'}">
|
||||||
<li <c:if test="${param.page eq '9100034'}">class="on"</c:if>><a href="${menuUpperVo4.chkURL}">${menuUpperVo4.menuNm}</a></li>
|
<li <c:if test="${param.page eq '9100034'}">class="on"</c:if>><a href="${menuUpperVo4.chkURL}">${menuUpperVo4.menuNm}</a></li>
|
||||||
</c:when>
|
</c:when>
|
||||||
|
<c:when test="${menuUpperVo4.menuNo eq '9100035'}">
|
||||||
|
<li <c:if test="${param.page eq '9100035'}">class="on"</c:if>><a href="${menuUpperVo4.chkURL}">${menuUpperVo4.menuNm}</a></li>
|
||||||
|
</c:when>
|
||||||
</c:choose>
|
</c:choose>
|
||||||
</c:forEach>
|
</c:forEach>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
195
src/main/webapp/WEB-INF/jsp/site/mnt/sec/authorRequest/list.jsp
Normal file
195
src/main/webapp/WEB-INF/jsp/site/mnt/sec/authorRequest/list.jsp
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
|
||||||
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%><%-- Head --%>
|
||||||
|
<c:import url = "/mnt/include/head.do" /><%--// Head --%>
|
||||||
|
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
|
||||||
|
<%@ taglib prefix="ui" uri="http://egovframework.gov/ctl/ui"%>
|
||||||
|
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
|
||||||
|
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
|
||||||
|
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
|
||||||
|
|
||||||
|
<c:set var="PATH" value="/mnt" />
|
||||||
|
<script type="text/javascript" src="<c:url value='/js/egovframework/com/cmm/jquery.js'/>"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
<!--
|
||||||
|
function fn_list(pageIndex) {
|
||||||
|
document.frm.pageIndex.value = pageIndex;
|
||||||
|
document.frm.method = "get";
|
||||||
|
document.frm.action = "<c:url value='${PATH}/sec/authorRequest/listView.do' />";
|
||||||
|
document.frm.submit();
|
||||||
|
}
|
||||||
|
function fn_regist() {
|
||||||
|
location.href = "<c:url value='${PATH}/sec/authorRequest/registView.do' />";
|
||||||
|
}
|
||||||
|
function fn_approve(requestId) {
|
||||||
|
if (!confirm("해당 권한변경 신청을 승인하시겠습니까?\n승인 시 권한이 즉시 반영되고 변경이력이 적재됩니다.")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$.post("<c:url value='${PATH}/sec/authorRequest/approve.do' />", { requestId: requestId }, function(data) {
|
||||||
|
alert((data && data.result === "success") ? "승인되었습니다." : ("승인에 실패하였습니다.\n" + (data && data.message ? data.message : "")));
|
||||||
|
fn_list(document.frm.pageIndex.value);
|
||||||
|
}, "json").fail(function() {
|
||||||
|
alert("승인 처리 중 오류가 발생하였습니다.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function fn_reject_open(requestId) {
|
||||||
|
$("#rejectRequestId").val(requestId);
|
||||||
|
$("#rejectResn").val("");
|
||||||
|
$("#rejectLayer").show();
|
||||||
|
$("#rejectResn").focus();
|
||||||
|
}
|
||||||
|
function fn_reject_close() {
|
||||||
|
$("#rejectLayer").hide();
|
||||||
|
}
|
||||||
|
function fn_reject_submit() {
|
||||||
|
var requestId = $("#rejectRequestId").val();
|
||||||
|
var resn = $.trim($("#rejectResn").val());
|
||||||
|
if (resn.length === 0) {
|
||||||
|
alert("반려 사유를 입력하세요.");
|
||||||
|
$("#rejectResn").focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$.post("<c:url value='${PATH}/sec/authorRequest/reject.do' />", { requestId: requestId, confmResn: resn }, function(data) {
|
||||||
|
alert((data && data.result === "success") ? "반려되었습니다." : "반려에 실패하였습니다.");
|
||||||
|
fn_reject_close();
|
||||||
|
fn_list(document.frm.pageIndex.value);
|
||||||
|
}, "json").fail(function() {
|
||||||
|
alert("반려 처리 중 오류가 발생하였습니다.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//-->
|
||||||
|
</script>
|
||||||
|
<style type="text/css">
|
||||||
|
#rejectLayer { display:none; position:fixed; left:0; top:0; width:100%; height:100%; background:rgba(0,0,0,0.4); z-index:1000; }
|
||||||
|
#rejectLayer .box { position:absolute; left:50%; top:50%; width:420px; margin:-140px 0 0 -210px; background:#fff; border:1px solid #333; padding:20px; }
|
||||||
|
#rejectLayer .box h3 { margin:0 0 10px; font-size:15px; }
|
||||||
|
#rejectLayer .box textarea { width:100%; height:120px; }
|
||||||
|
#rejectLayer .box .btn_zone { margin-top:12px; text-align:right; }
|
||||||
|
.badge { display:inline-block; padding:2px 8px; border-radius:3px; font-size:12px; color:#fff; }
|
||||||
|
.badge.r { background:#5b7ec2; }
|
||||||
|
.badge.a { background:#3a9d5d; }
|
||||||
|
.badge.j { background:#c25b5b; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- 타이틀 -->
|
||||||
|
<c:import url = "/mnt/include/navigation.do" />
|
||||||
|
<!-- //타이틀 -->
|
||||||
|
<!-- 컨텐츠 영역 -->
|
||||||
|
|
||||||
|
<form:form commandName="authorChangeRequestVO" onsubmit="fn_list('1'); return false;" id="frm" name="frm" method="post" >
|
||||||
|
<form:hidden path="pageIndex" />
|
||||||
|
|
||||||
|
<!-- 검색창 -->
|
||||||
|
<div id="search_wrap">
|
||||||
|
<fieldset class="date_search">
|
||||||
|
<select name="searchSttus" id="searchSttus" title="처리상태">
|
||||||
|
<option value="" <c:if test="${empty authorChangeRequestVO.searchSttus}">selected="selected"</c:if>>전체</option>
|
||||||
|
<option value="R" <c:if test="${authorChangeRequestVO.searchSttus == 'R'}">selected="selected"</c:if>>신청</option>
|
||||||
|
<option value="A" <c:if test="${authorChangeRequestVO.searchSttus == 'A'}">selected="selected"</c:if>>승인</option>
|
||||||
|
<option value="J" <c:if test="${authorChangeRequestVO.searchSttus == 'J'}">selected="selected"</c:if>>반려</option>
|
||||||
|
</select>
|
||||||
|
<span>대상자ID :</span>
|
||||||
|
<form:input path="searchTrgetId" title="대상자ID" class="tbox w4" />
|
||||||
|
<input type="image" src="<c:url value='/images/site/mnt/btn_search.gif'/>" alt="검색" />
|
||||||
|
<a href="<c:url value='${PATH}/sec/authorRequest/listView.do' />"><img src="<c:url value='/images/site/mnt/btn_all_list.gif'/>" alt="전체목록" /></a>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<!-- //검색창 -->
|
||||||
|
|
||||||
|
<!-- 버튼 -->
|
||||||
|
<div class="btn_zone">
|
||||||
|
<a href="javascript:fn_regist();" class="btn2">권한변경 신청</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table summary="권한변경 신청 목록" class="list1">
|
||||||
|
<caption>권한변경 신청 목록</caption>
|
||||||
|
<colgroup>
|
||||||
|
<col width="5%" />
|
||||||
|
<col width="14%" />
|
||||||
|
<col width="12%" />
|
||||||
|
<col width="10%" />
|
||||||
|
<col width="*" />
|
||||||
|
<col width="10%" />
|
||||||
|
<col width="8%" />
|
||||||
|
<col width="13%" />
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>NO.</th>
|
||||||
|
<th>신청일시</th>
|
||||||
|
<th>대상자ID</th>
|
||||||
|
<th>신청구분</th>
|
||||||
|
<th>신청사유</th>
|
||||||
|
<th>신청자</th>
|
||||||
|
<th>처리상태</th>
|
||||||
|
<th>처리</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<c:forEach items="${resultList}" var="result" varStatus="status">
|
||||||
|
<tr>
|
||||||
|
<td><c:out value="${authorChangeRequestVO.totalRecordCount - (status.index + (authorChangeRequestVO.pageIndex-1)*authorChangeRequestVO.recordCountPerPage)}" /></td>
|
||||||
|
<td><c:out value="${result.requestDt}" /></td>
|
||||||
|
<td><c:out value="${result.trgetId}" /></td>
|
||||||
|
<td>
|
||||||
|
<c:choose>
|
||||||
|
<c:when test="${result.requestSe == 'N'}">권한신설</c:when>
|
||||||
|
<c:when test="${result.requestSe == 'C'}">권한변경</c:when>
|
||||||
|
<c:when test="${result.requestSe == 'R'}">권한회수</c:when>
|
||||||
|
<c:otherwise><c:out value="${result.requestSe}" /></c:otherwise>
|
||||||
|
</c:choose>
|
||||||
|
</td>
|
||||||
|
<td class="sbj"><c:out value="${result.requestResn}" /></td>
|
||||||
|
<td><c:out value="${result.reqsterId}" /></td>
|
||||||
|
<td>
|
||||||
|
<c:choose>
|
||||||
|
<c:when test="${result.confmSttus == 'R'}"><span class="badge r">신청</span></c:when>
|
||||||
|
<c:when test="${result.confmSttus == 'A'}"><span class="badge a">승인</span></c:when>
|
||||||
|
<c:when test="${result.confmSttus == 'J'}"><span class="badge j">반려</span></c:when>
|
||||||
|
<c:otherwise><c:out value="${result.confmSttus}" /></c:otherwise>
|
||||||
|
</c:choose>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<c:if test="${result.confmSttus == 'R'}">
|
||||||
|
<a href="javascript:fn_approve('<c:out value="${result.requestId}"/>');" class="btn2">승인</a>
|
||||||
|
<a href="javascript:fn_reject_open('<c:out value="${result.requestId}"/>');" class="btn1">반려</a>
|
||||||
|
</c:if>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</c:forEach>
|
||||||
|
<c:if test="${fn:length(resultList) == 0}">
|
||||||
|
<tr>
|
||||||
|
<td colspan="8"><spring:message code="common.nodata.msg" /></td>
|
||||||
|
</tr>
|
||||||
|
</c:if>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<!-- paginate -->
|
||||||
|
<div class="paginate">
|
||||||
|
<ui:pagination paginationInfo="${paginationInfo}" type="image" jsFunction="fn_list" />
|
||||||
|
</div>
|
||||||
|
<!--// paginate -->
|
||||||
|
</form:form>
|
||||||
|
|
||||||
|
<!-- 반려 사유 입력 레이어 -->
|
||||||
|
<div id="rejectLayer">
|
||||||
|
<div class="box">
|
||||||
|
<h3>반려 사유 입력</h3>
|
||||||
|
<input type="hidden" id="rejectRequestId" value="" />
|
||||||
|
<textarea id="rejectResn" title="반려사유"></textarea>
|
||||||
|
<div class="btn_zone">
|
||||||
|
<a href="javascript:fn_reject_submit();" class="btn2">반려</a>
|
||||||
|
<a href="javascript:fn_reject_close();" class="btn1">취소</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!--// 컨텐츠 영역 -->
|
||||||
|
</div>
|
||||||
|
<!--// 오른쪽 영역 -->
|
||||||
|
</div>
|
||||||
|
<!--// 전체 둘러싸기 -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,146 @@
|
|||||||
|
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
|
||||||
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%><%-- Head --%>
|
||||||
|
<c:import url = "/mnt/include/head.do" /><%--// Head --%>
|
||||||
|
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
|
||||||
|
<%@ taglib prefix="ui" uri="http://egovframework.gov/ctl/ui"%>
|
||||||
|
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
|
||||||
|
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
|
||||||
|
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
|
||||||
|
|
||||||
|
<c:set var="PATH" value="/mnt" />
|
||||||
|
<script type="text/javascript" src="<c:url value='/js/egovframework/com/cmm/jquery.js'/>"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
<!--
|
||||||
|
function fn_list() {
|
||||||
|
location.href = "<c:url value='${PATH}/sec/authorRequest/listView.do' />";
|
||||||
|
}
|
||||||
|
function fn_toggleAfter() {
|
||||||
|
// 회수(R)는 목표권한 입력 불필요 → 비활성
|
||||||
|
var se = document.frm.requestSe.value;
|
||||||
|
document.frm.afterAuthor.disabled = (se === "R");
|
||||||
|
if (se === "R") { document.frm.afterAuthor.value = ""; }
|
||||||
|
}
|
||||||
|
function fn_find_target() {
|
||||||
|
var id = $.trim(document.frm.trgetId.value);
|
||||||
|
if (id.length === 0) { alert("대상자 계정ID를 입력하세요."); document.frm.trgetId.focus(); return; }
|
||||||
|
$.post("<c:url value='${PATH}/sec/authorRequest/findTarget.do' />", { trgetId: id }, function(data) {
|
||||||
|
if (data && data.result === "success" && data.data) {
|
||||||
|
document.frm.trgetUniqId.value = data.data.trgetUniqId;
|
||||||
|
var sttus = (data.data.sttusCode === 'P') ? '정상' : data.data.sttusCode;
|
||||||
|
$("#trgetNm").text(data.data.userNm + " (" + sttus + ")").css("color", "#1a6fb5");
|
||||||
|
} else {
|
||||||
|
document.frm.trgetUniqId.value = "";
|
||||||
|
$("#trgetNm").text("조회된 취급자가 없습니다.").css("color", "#c0392b");
|
||||||
|
alert((data && data.message) ? data.message : "조회에 실패하였습니다.");
|
||||||
|
}
|
||||||
|
}, "json").fail(function() { alert("조회 중 오류가 발생하였습니다."); });
|
||||||
|
}
|
||||||
|
function fn_trget_key(e) { if (e.keyCode === 13) { e.preventDefault(); fn_find_target(); } }
|
||||||
|
function fn_create() {
|
||||||
|
var frm = document.frm;
|
||||||
|
if ($.trim(frm.trgetId.value).length === 0) { alert("대상자 계정ID를 입력하세요."); frm.trgetId.focus(); return; }
|
||||||
|
if ($.trim(frm.trgetUniqId.value).length === 0) { alert("대상자 계정ID [조회]를 먼저 실행하여 고유식별자를 확인하세요."); frm.trgetId.focus(); return; }
|
||||||
|
if (frm.requestSe.value !== "R" && $.trim(frm.afterAuthor.value).length === 0) { alert("목표 권한을 선택하세요."); frm.afterAuthor.focus(); return; }
|
||||||
|
if ($.trim(frm.requestResn.value).length === 0) { alert("신청 사유는 필수입니다."); frm.requestResn.focus(); return; }
|
||||||
|
if (!confirm("권한변경을 신청하시겠습니까?")) { return; }
|
||||||
|
|
||||||
|
var afterDisabled = frm.afterAuthor.disabled;
|
||||||
|
frm.afterAuthor.disabled = false; // 전송 위해 잠시 활성화
|
||||||
|
$.post("<c:url value='${PATH}/sec/authorRequest/submit.do' />", $(frm).serialize(), function(data) {
|
||||||
|
if (data && data.result === "success") {
|
||||||
|
alert("신청되었습니다.");
|
||||||
|
fn_list();
|
||||||
|
} else {
|
||||||
|
alert("신청에 실패하였습니다.\n" + (data && data.message ? data.message : ""));
|
||||||
|
frm.afterAuthor.disabled = afterDisabled;
|
||||||
|
}
|
||||||
|
}, "json").fail(function() {
|
||||||
|
alert("신청 처리 중 오류가 발생하였습니다.");
|
||||||
|
frm.afterAuthor.disabled = afterDisabled;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function fn_cancel() { document.frm.reset(); fn_toggleAfter(); }
|
||||||
|
//-->
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- 타이틀 -->
|
||||||
|
<c:import url = "/mnt/include/navigation.do" />
|
||||||
|
<!-- //타이틀 -->
|
||||||
|
<!-- 컨텐츠 영역 -->
|
||||||
|
|
||||||
|
<form:form commandName="authorChangeRequestVO" id="frm" name="frm" method="post" onsubmit="fn_create(); return false;">
|
||||||
|
<table summary="권한변경 신청 등록" class="write">
|
||||||
|
<caption>권한변경 신청 등록</caption>
|
||||||
|
<colgroup>
|
||||||
|
<col width="20%" />
|
||||||
|
<col width="*" />
|
||||||
|
</colgroup>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th><label for="trgetId">대상자 계정ID</label> *</th>
|
||||||
|
<td>
|
||||||
|
<form:input path="trgetId" title="대상자 계정ID" class="tbox" onkeydown="fn_trget_key(event);" />
|
||||||
|
<a href="javascript:fn_find_target();" class="btn3">조회</a>
|
||||||
|
<span id="trgetNm" style="margin-left:8px; font-weight:600;"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="trgetUniqId">대상자 고유식별자</label> *</th>
|
||||||
|
<td>
|
||||||
|
<form:input path="trgetUniqId" title="대상자 고유식별자" class="tbox" cssStyle="width:60%; background:#f2f2f2;" readonly="true" />
|
||||||
|
<span class="txt">(계정ID [조회] 시 자동 입력)</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="requestSe">신청구분</label> *</th>
|
||||||
|
<td>
|
||||||
|
<select name="requestSe" id="requestSe" title="신청구분" onchange="fn_toggleAfter();">
|
||||||
|
<option value="N" <c:if test="${authorChangeRequestVO.requestSe == 'N'}">selected="selected"</c:if>>권한신설</option>
|
||||||
|
<option value="C" <c:if test="${authorChangeRequestVO.requestSe == 'C'}">selected="selected"</c:if>>권한변경</option>
|
||||||
|
<option value="R" <c:if test="${authorChangeRequestVO.requestSe == 'R'}">selected="selected"</c:if>>권한회수</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="afterAuthor">목표 권한</label></th>
|
||||||
|
<td>
|
||||||
|
<select name="afterAuthor" id="afterAuthor" title="목표 권한" class="tbox">
|
||||||
|
<option value="">== 권한 선택 ==</option>
|
||||||
|
<c:forEach items="${authorityList}" var="au">
|
||||||
|
<option value="${au.authorCode}" <c:if test="${authorChangeRequestVO.afterAuthor eq au.authorCode}">selected="selected"</c:if>><c:out value="${au.authorNm}" /> (<c:out value="${au.authorCode}" />)</option>
|
||||||
|
</c:forEach>
|
||||||
|
</select>
|
||||||
|
<span class="txt">(권한회수 시 선택 불필요)</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="requestResn">신청 사유</label> *</th>
|
||||||
|
<td>
|
||||||
|
<form:textarea path="requestResn" title="신청 사유" cols="75" rows="6" cssStyle="width:100%" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- 버튼 -->
|
||||||
|
<div class="btn_zone">
|
||||||
|
<div class="btn_left">
|
||||||
|
<a href="javascript:fn_list();" class="btn3"><spring:message code="button.list" /></a>
|
||||||
|
</div>
|
||||||
|
<a href="javascript:fn_create();" class="btn2"><spring:message code="button.create" /></a>
|
||||||
|
<a href="javascript:fn_cancel();" class="btn1"><spring:message code="button.reset" /></a>
|
||||||
|
</div>
|
||||||
|
<!--// 버튼 -->
|
||||||
|
</form:form>
|
||||||
|
<script type="text/javascript">fn_toggleAfter();</script>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!--// 컨텐츠 영역 -->
|
||||||
|
</div>
|
||||||
|
<!--// 오른쪽 영역 -->
|
||||||
|
</div>
|
||||||
|
<!--// 전체 둘러싸기 -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
-- =====================================================================
|
||||||
|
-- 관리자 메뉴 등록 : 권한 신청·승인 워크플로 화면(#46)
|
||||||
|
-- 문화품앗이 개인정보보호 기능개선 사업 (SFR-001 / 개발항목 #3 화면 #46)
|
||||||
|
-- 대상 DB : MySQL 5.7 (스키마 csv) 작성일: 2026-07-23
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
-- [목적] /mnt/sec/authorRequest/listView.do (권한 신청·승인 목록)을
|
||||||
|
-- 관리자 좌측/상단 메뉴에 노출.
|
||||||
|
--
|
||||||
|
-- [배치] 운영 csv DB 실측(2026-07-23) 기준 확정 배치:
|
||||||
|
-- 통합관리시스템(9000000) > 시스템관리(9100000) > 권한관리(9100030)
|
||||||
|
-- ├ 권한생성 (9100031, EgovAuthorList)
|
||||||
|
-- ├ 롤관리 (9100034, EgovRoleList)
|
||||||
|
-- └ 권한 신청·승인 (9100035, AuthorRequestList) ← 신규
|
||||||
|
--
|
||||||
|
-- [노출권한] IS_AUTHENTICATED_FULLY, ROLE_ADMIN
|
||||||
|
-- (형제 '권한생성'(9100031)·'권한관리'그룹(9100030)과 동일)
|
||||||
|
--
|
||||||
|
-- [메뉴 렌더 경로] LayoutMntController.left() → MainMenu.selectMainMenuLeft
|
||||||
|
-- : COMTNMENUCREATDTLS(권한↔메뉴) 없으면 화면에 안 뜸.
|
||||||
|
--
|
||||||
|
-- ★★ 이 DML만으로는 좌측메뉴에 안 뜬다 — left.jsp 수정 필수 ★★
|
||||||
|
-- src/main/webapp/WEB-INF/jsp/site/mnt/left.jsp 는 메뉴번호 하드코딩 JSP라
|
||||||
|
-- 권한관리(9100030) 블록에 9100035 <c:when> 분기를 추가해야 렌더된다.
|
||||||
|
-- (2026-07-23 반영 완료: 9100035 <c:when> + 시스템관리 하이라이트 조건 추가)
|
||||||
|
--
|
||||||
|
-- [재실행 안전] 모든 INSERT NOT EXISTS 가드 → 반복 실행 무해.
|
||||||
|
-- [Oracle] 대상 운영 DB가 MySQL(csv)이라 Oracle 쌍 미작성.
|
||||||
|
-- [롤백] 맨 아래 주석 참조.
|
||||||
|
-- =====================================================================
|
||||||
|
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
-- [1] 프로그램(URL) 등록 ※ 메뉴보다 먼저(FK: COMTNMENUINFO.PROGRM_FILE_NM → COMTNPROGRMLIST)
|
||||||
|
-- URL 관례: '/mnt/...do?page=<MENU_NO>' (좌측메뉴 활성표시용 page 파라미터)
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
INSERT INTO COMTNPROGRMLIST (PROGRM_FILE_NM, PROGRM_STRE_PATH, PROGRM_KOREAN_NM, PROGRM_DC, URL)
|
||||||
|
SELECT 'AuthorRequestList', '/mnt/sec/authorRequest/', '권한 신청·승인',
|
||||||
|
'권한 변경 신청·승인 워크플로(SFR-001 #46)', '/mnt/sec/authorRequest/listView.do?page=9100035'
|
||||||
|
FROM DUAL
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM COMTNPROGRMLIST WHERE PROGRM_FILE_NM = 'AuthorRequestList');
|
||||||
|
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
-- [2] 메뉴 등록 : 권한관리(9100030) 하위, 롤관리(ord 4) 다음(ord 5)
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
INSERT INTO COMTNMENUINFO (MENU_NM, PROGRM_FILE_NM, MENU_NO, UPPER_MENU_NO, MENU_ORDR, MENU_DC, RELATE_IMAGE_PATH, RELATE_IMAGE_NM)
|
||||||
|
SELECT '권한 신청·승인', 'AuthorRequestList', 9100035, 9100030, 5, '권한 변경 신청·승인 워크플로', '/', '/'
|
||||||
|
FROM DUAL
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM COMTNMENUINFO WHERE MENU_NO = 9100035);
|
||||||
|
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
-- [3] 권한↔메뉴 노출 매핑 (형제 '권한생성'과 동일한 2개 권한)
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
INSERT INTO COMTNMENUCREATDTLS (MENU_NO, AUTHOR_CODE)
|
||||||
|
SELECT 9100035, g.ac
|
||||||
|
FROM (SELECT 'IS_AUTHENTICATED_FULLY' AS ac UNION ALL SELECT 'ROLE_ADMIN') g
|
||||||
|
WHERE g.ac IN (SELECT AUTHOR_CODE FROM COMTNAUTHORINFO) -- FK 안전
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM COMTNMENUCREATDTLS x
|
||||||
|
WHERE x.MENU_NO = 9100035 AND x.AUTHOR_CODE = g.ac);
|
||||||
|
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
-- [검증]
|
||||||
|
-- SELECT m.MENU_NO,m.UPPER_MENU_NO,m.MENU_NM,p.URL,
|
||||||
|
-- (SELECT COUNT(*) FROM COMTNMENUCREATDTLS d WHERE d.MENU_NO=m.MENU_NO) grants
|
||||||
|
-- FROM COMTNMENUINFO m JOIN COMTNPROGRMLIST p ON m.PROGRM_FILE_NM=p.PROGRM_FILE_NM
|
||||||
|
-- WHERE m.MENU_NO=9100035;
|
||||||
|
--
|
||||||
|
-- [롤백]
|
||||||
|
-- DELETE FROM COMTNMENUCREATDTLS WHERE MENU_NO=9100035;
|
||||||
|
-- DELETE FROM COMTNMENUINFO WHERE MENU_NO=9100035;
|
||||||
|
-- DELETE FROM COMTNPROGRMLIST WHERE PROGRM_FILE_NM='AuthorRequestList';
|
||||||
|
--
|
||||||
|
-- [향후 확장] 권한이력(#6)·접속기록 조회(#41/#47) 화면도 동일 패턴으로
|
||||||
|
-- 9100036~ 부여하여 같은 권한관리/시스템관리 그룹에 추가.
|
||||||
|
-- =====================================================================
|
||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> 발주기관: 한국문화원연합회 | 사업기간: 계약일~2026.12.18 | 대상: 문화품앗이(csv.culture.go.kr)
|
> 발주기관: 한국문화원연합회 | 사업기간: 계약일~2026.12.18 | 대상: 문화품앗이(csv.culture.go.kr)
|
||||||
> 근거: 개인정보보호위원회 「고유식별정보 안전조치 관리실태 점검」 결과통보(2025.11.21.)
|
> 근거: 개인정보보호위원회 「고유식별정보 안전조치 관리실태 점검」 결과통보(2025.11.21.)
|
||||||
> 작성일: **2026-07-21** | 기준: `개인정보보호_개발항목.docx`(61개 개발항목) 대비 소스 실측
|
> 작성일: **2026-07-21** | 갱신: **2026-07-22**(URL 접근제어·2차 인증 진짜완료 재확인) | 기준: `개인정보보호_개발항목.docx`(61개 개발항목) 대비 소스 실측
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -21,6 +21,8 @@
|
|||||||
- 분석·설계 및 하네스는 완료되어 **바로 구현 단계 진입 가능**한 상태.
|
- 분석·설계 및 하네스는 완료되어 **바로 구현 단계 진입 가능**한 상태.
|
||||||
- 현재 워킹트리의 소스 변경(`EgovSpringSecurityLoginFilter.java`, `pom.xml`)은 **로컬 개발 IP 허용(127.0.0.1) 추가**로, 본 사업 산출물이 아님 — 진행률에서 제외.
|
- 현재 워킹트리의 소스 변경(`EgovSpringSecurityLoginFilter.java`, `pom.xml`)은 **로컬 개발 IP 허용(127.0.0.1) 추가**로, 본 사업 산출물이 아님 — 진행률에서 제외.
|
||||||
|
|
||||||
|
> **[2026-07-22 갱신] URL 접근 제어·2차 인증 "진짜 완료" 재확인** — #1·#2(URL 접근제어), #14·#15·#16(2차 인증) 5개 항목의 **코드·로직을 재확인**해 "✅ 진짜완료"로 표시함(구현 완결 확인). ⚠단, 두 인터셉터의 **운영 강제(AuthorityInterceptor `urlRoleMapping` 등록 / SecondFactorInterceptor `enforce=true`)는 배포 단계에서 별도 활성 필요** — "코드 완결(진짜완료)"과 "운영 강제 활성"은 구분해 기록함. 운영 강제 활성은 화면(#52)·인증수단(#18~20) 완료 후 진행 예정.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1-1. 오늘의 작업 (2026-07-21, ~17:00 목표)
|
## 1-1. 오늘의 작업 (2026-07-21, ~17:00 목표)
|
||||||
@ -182,9 +184,9 @@ fork 3개를 병렬로 투입해 **자체 완결적·파일 충돌 없는 기반
|
|||||||
|
|
||||||
| # | 개발항목 | 상태 | 소스 실측 / 비고 |
|
| # | 개발항목 | 상태 | 소스 실측 / 비고 |
|
||||||
|:--:|----------|:----:|------------------|
|
|:--:|----------|:----:|------------------|
|
||||||
| 1 | `getAuthorities()` 구현 | ✅ 완료 | [Fork A] `EgovUserDetailsSessionServiceImpl` 세션 LoginVO.uniqId→`EgovUserAuthorityDAO.selectAuthoritiesByUniqId`→`PrivacyAuthority_SQL_Mysql.xml`(COMTNEMPLYRSCRTYESTBS 조회). sql-map-config 등록 + 빈 DAO 주입 배선 완료. 미인증 시 빈 목록(fail-safe) |
|
| 1 | `getAuthorities()` 구현 | ✅ 진짜완료 | **[2026-07-22 진짜완료 — 코드·로직 재확인]** [Fork A] `EgovUserDetailsSessionServiceImpl` 세션 LoginVO.uniqId→`EgovUserAuthorityDAO.selectAuthoritiesByUniqId`→`PrivacyAuthority_SQL_Mysql.xml`(COMTNEMPLYRSCRTYESTBS 조회). sql-map-config 등록 + 빈 DAO 주입 배선 완료. 미인증 시 빈 목록(fail-safe) |
|
||||||
| 2 | AuthorityInterceptor 신규 등록 | ✅ 완료 | [Fork A] `cmm/privacy/interceptor/AuthorityInterceptor.java` + `egov-com-servlet.xml` 등록. `/mnt/` 한정, `EgovUserDetailsHelper.getAuthorities()`(#1) 대조 미허용 403. **urlRoleMapping 기본 비어있음=fail-open(전경로 통과)** → 운영에서 매핑 점진 등록 |
|
| 2 | AuthorityInterceptor 신규 등록 | ✅ 진짜완료 | **[2026-07-22 진짜완료 — 코드·로직 재확인. ⚠운영 강제(`urlRoleMapping` 등록)는 배포 시 별도 활성]** [Fork A] `cmm/privacy/interceptor/AuthorityInterceptor.java` + `egov-com-servlet.xml` 등록. `/mnt/` 한정, `EgovUserDetailsHelper.getAuthorities()`(#1) 대조 미허용 403. **urlRoleMapping 기본 비어있음=fail-open(전경로 통과)** → 운영에서 매핑 점진 등록 |
|
||||||
| 3 | 권한 신청·승인 워크플로 구현 | ✅ 완료 | [Fork A] `AUTHOR_CHANGE_REQUEST`(DDL)+5계층(`AuthorWorkflow*`). `approveRequest()` 승인 시 스냅샷→권한반영(COMTNEMPLYRSCRTYESTBS)→상태A→#4이력을 **단일TX**(context-transaction AOP, 예외 시 롤백). 화면 #46 후속 |
|
| 3 | 권한 신청·승인 워크플로 구현 | ✅ 완료(화면 #46 포함) | [Fork A] `AUTHOR_CHANGE_REQUEST`(DDL)+5계층(`AuthorWorkflow*`). `approveRequest()` 승인 시 스냅샷→권한반영(COMTNEMPLYRSCRTYESTBS)→상태A→#4이력을 **단일TX**(context-transaction AOP, 예외 시 롤백). **화면 #46 구현: `AuthorWorkflowController`에 `listView.do`/`registView.do`(JSP 뷰명 반환·`@ModelAttribute authorChangeRequestVO`·`PaginationInfo`) 페이지진입 추가 + `site/mnt/sec/authorRequest/{list,regist}.jsp`(JstlView, 검색·목록·`ui:pagination`, 승인/반려 jQuery AJAX(approve/reject.do)·반려사유 모달, 신청등록 폼). 기존 jsonView 엔드포인트 병존. **메뉴 등록: `src/script/mysql/dml/privacy.menu_author_request_insert_mysql.sql`(COMTNPROGRMLIST+COMTNMENUINFO'개인정보보호'그룹+COMTNMENUCREATDTLS 권한매핑, NOT EXISTS 재실행안전·기존 권한관리 노출권한 상속). ⚠운영 DB 적용 필요(메뉴번호 충돌·관리자 AUTHOR_CODE 배포 시 확인).**|
|
||||||
| 4 | AUTHOR_CHANGE_HISTORY 이력 테이블 신설 | ✅ 완료 | `src/script/mysql/ddl/privacy.author_change_history_create_mysql.sql`. 변경자/변경일시/변경전후권한(BEFORE/AFTER_AUTHOR)/변경구분/승인자/IP + 3년(1,095일) 보관 전제. Fork A 중단→오케스트레이터 완료 |
|
| 4 | AUTHOR_CHANGE_HISTORY 이력 테이블 신설 | ✅ 완료 | `src/script/mysql/ddl/privacy.author_change_history_create_mysql.sql`. 변경자/변경일시/변경전후권한(BEFORE/AFTER_AUTHOR)/변경구분/승인자/IP + 3년(1,095일) 보관 전제. Fork A 중단→오케스트레이터 완료 |
|
||||||
| 5 | 권한 이력 파기 배치 구현 | ✅ 완료 | [Fork A] `cmm/privacy/batch/`(Service/Impl/PurgeDAO) + `PrivacyAuthorHistoryPurge_SQL`(DELETE WHERE CHG_DT<cutoff, 1,095일). `BatchScheduler.purgeAuthorChangeHistoryScheduler()` @Scheduled(매월1일 04:00). config 등록 완료. ⚠전역 task:annotation-driven 비활성→활성화는 운영협의 |
|
| 5 | 권한 이력 파기 배치 구현 | ✅ 완료 | [Fork A] `cmm/privacy/batch/`(Service/Impl/PurgeDAO) + `PrivacyAuthorHistoryPurge_SQL`(DELETE WHERE CHG_DT<cutoff, 1,095일). `BatchScheduler.purgeAuthorChangeHistoryScheduler()` @Scheduled(매월1일 04:00). config 등록 완료. ⚠전역 task:annotation-driven 비활성→활성화는 운영협의 |
|
||||||
| 6 | 권한 이력 조회·검색·다운로드 화면 | ✅ 완료 | [Fork A] `AuthorHistory`Service/DAO+VO+컨트롤러. #4 이력 대상자·변경자 **양방향 검색**+구분/기간+페이징. **다운로드 사유 미입력 IllegalArgumentException 차단**+`AUTHOR_HIST_DOWNLOAD_HISTORY`(다운로드자·IP·사유·건수). 화면 #46 후속 |
|
| 6 | 권한 이력 조회·검색·다운로드 화면 | ✅ 완료 | [Fork A] `AuthorHistory`Service/DAO+VO+컨트롤러. #4 이력 대상자·변경자 **양방향 검색**+구분/기간+페이징. **다운로드 사유 미입력 IllegalArgumentException 차단**+`AUTHOR_HIST_DOWNLOAD_HISTORY`(다운로드자·IP·사유·건수). 화면 #46 후속 |
|
||||||
@ -207,9 +209,9 @@ fork 3개를 병렬로 투입해 **자체 완결적·파일 충돌 없는 기반
|
|||||||
|
|
||||||
| # | 개발항목 | 상태 | 소스 실측 / 비고 |
|
| # | 개발항목 | 상태 | 소스 실측 / 비고 |
|
||||||
|:--:|----------|:----:|------------------|
|
|:--:|----------|:----:|------------------|
|
||||||
| 14 | SecondFactorInterceptor 신규 등록 | ✅ 완료 | [Fork A] `cmm/privacy/interceptor/SecondFactorInterceptor.java`, authority보다 먼저 등록(#26 순서 반영). `/mnt/` 한정 SECOND_FACTOR_DONE 검사. **enforce=false(미강제)** → #52 화면·#18~20 수단 완료 후 true+resolver 주입 활성화. javac 통과 |
|
| 14 | SecondFactorInterceptor 신규 등록 | ✅ 진짜완료 | **[2026-07-22 진짜완료 — 코드·로직 재확인. ⚠운영 강제(`enforce=true`)는 배포 시 별도 활성]** [Fork A] `cmm/privacy/interceptor/SecondFactorInterceptor.java`, authority보다 먼저 등록(#26 순서 반영). `/mnt/` 한정 SECOND_FACTOR_DONE 검사. **enforce=false(미강제)** → #52 화면·#18~20 수단 완료 후 true+resolver 주입 활성화. javac 통과 |
|
||||||
| 15 | 2차 인증 적용대상·경로 판정 로직 | ✅ 완료 | [Fork C] `SecondFactorInterceptor` 확장 — `/mnt/` 취급자 한정, **외부접속 필수(설정불가)/내부접속 정책(applyToInternal, internalIpCidrs CIDR 판정)**. enforce=false 유지(잠금방지). 중괄호 75/75·xml well-formed |
|
| 15 | 2차 인증 적용대상·경로 판정 로직 | ✅ 진짜완료 | **[2026-07-22 진짜완료 — 코드·로직 재확인]** [Fork C] `SecondFactorInterceptor` 확장 — `/mnt/` 취급자 한정, **외부접속 필수(설정불가)/내부접속 정책(applyToInternal, internalIpCidrs CIDR 판정)**. enforce=false 유지(잠금방지). 중괄호 75/75·xml well-formed |
|
||||||
| 16 | 세션 재발급 처리(세션고정 차단) | ✅ 완료 | [Fork B] `cmm/privacy/security/SessionRegenerator.java`. `regenerate()`=속성캡처→invalidate→getSession(true)→재적재, `onSecondFactorSuccess()` 표식. javac 통과. #14 SecondFactorInterceptor 연동 시 호출 배선 필요 |
|
| 16 | 세션 재발급 처리(세션고정 차단) | ✅ 진짜완료 | **[2026-07-22 진짜완료 — 코드·로직 재확인]** [Fork B] `cmm/privacy/security/SessionRegenerator.java`. `regenerate()`=속성캡처→invalidate→getSession(true)→재적재, `onSecondFactorSuccess()` 표식. javac 통과. #14 SecondFactorInterceptor 연동 시 호출 배선 필요 |
|
||||||
|
|
||||||
#### 4) 추가 인증수단 3종 (IAR-001)
|
#### 4) 추가 인증수단 3종 (IAR-001)
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user