충돌 해결

This commit is contained in:
hong
2026-07-27 17:44:23 +09:00
44 changed files with 29183 additions and 583 deletions
+4 -4
View File
@@ -369,12 +369,12 @@ page:
credential_detail:
name: "인증키 정보"
path: "/myapikey/credential_detail"
change_password:
password_verify:
name: "비밀번호 변경"
path: "/change_password"
verify_current_password:
path: "/password/verify"
password_change:
name: "비밀번호 변경"
path: "/verify_current_password"
path: "/password/change"
myapikey_register_step1:
name: "앱 생성 (기본 정보)"
path: "/myapikey/register/step1"
+20
View File
@@ -69,6 +69,24 @@
</encoder>
</appender>
<!-- ERROR 레벨만 별도 수집(스택 트레이스 포함). 장애 원인 추적용. -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/error.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/backup/error.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>200MB</maxFileSize>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
</encoder>
</appender>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>${CONSOLE_EFFECTIVE_LEVEL}</level>
@@ -89,12 +107,14 @@
<root level="INFO">
<appender-ref ref="ROLLING"/>
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
<springProfile name="dev">
<root level="DEBUG">
<appender-ref ref="ROLLING"/>
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</springProfile>
</configuration>
@@ -39,7 +39,7 @@ deptUserManageRegister.id=Department User ID
deptUserManageRegister.name=Department User Name
portalUser.confirm.password=Please enter your existing password
portalUser.Register.userName=Name
portalUser.Register.pass=Password (Combination of uppercase letters, lowercase letters, numbers, special characters, 8-20 characters)
portalUser.Register.pass=Password (Combination of uppercase letters, lowercase letters, numbers, special characters, 8-50 characters)
portalUser.Register.passConfirm=Confirm Password
portalUser.Register.email=Email ID
portalUser.Register.domain=Domain
@@ -39,7 +39,7 @@ deptUserManageRegister.name=\uBD80\uC11C \uC0AC\uC6A9\uC790 \uC774\uB984
entrprsUserManageList.regName=\uBC95\uC778 \uC0AC\uC6A9\uC790 \uB4F1\uB85D \uC774\uB984
portalUser.confirm.password=\uAE30\uC874 \uBE44\uBC00\uBC88\uD638\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694
portalUser.Register.userName=\uC774\uB984
portalUser.Register.pass=\uC601\uBB38 \uB300\uBB38\uC790,\uC601\uBB38 \uC18C\uBB38\uC790,\uC22B\uC790,\uD2B9\uC218\uBB38\uC790 \uC870\uD569 8-20\uC790
portalUser.Register.pass=\uC601\uBB38 \uB300\uBB38\uC790,\uC601\uBB38 \uC18C\uBB38\uC790,\uC22B\uC790,\uD2B9\uC218\uBB38\uC790 \uC870\uD569 8-50\uC790
portalUser.Register.passConfirm=\uBE44\uBC00\uBC88\uD638 \uD655\uC778
portalUser.Register.email=\uC774\uBA54\uC77C \uC544\uC774\uB514
portalUser.Register.domain=\uB3C4\uBA54\uC778
+69
View File
@@ -7571,6 +7571,69 @@ button.djb-comment-submit:disabled {
font-size: 13px;
}
.password-policy-checklist {
list-style: none;
padding: 0;
margin: 12px 0 0 0;
}
.password-policy-checklist li {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
line-height: 20px;
margin-bottom: 6px;
transition: color 0.15s ease;
}
.password-policy-checklist li:last-child {
margin-bottom: 0;
}
.password-policy-checklist li .policy-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
flex-shrink: 0;
font-size: 13px;
font-weight: 700;
line-height: 1;
}
.password-policy-checklist li .policy-icon::before {
content: "•";
}
.password-policy-checklist li.is-idle {
color: #888;
}
.password-policy-checklist li.is-idle .policy-icon {
color: #b5b5b5;
}
.password-policy-checklist li.is-pass {
color: #1a8f4c;
}
.password-policy-checklist li.is-pass .policy-icon {
color: #1a8f4c;
}
.password-policy-checklist li.is-pass .policy-icon::before {
content: "✔";
}
.password-policy-checklist li.is-fail {
color: #d63a3a;
}
.password-policy-checklist li.is-fail .policy-icon {
color: #d63a3a;
}
.password-policy-checklist li.is-fail .policy-icon::before {
content: "✖";
}
.password-policy-note {
margin: 10px 0 0 0;
font-size: 14px;
line-height: 18px;
color: #888;
}
.hero-carousel-section {
position: relative;
width: 100%;
@@ -17995,6 +18058,12 @@ input[type=checkbox]:checked + .custom-checkbox {
font-size: 14px;
margin: 0;
}
.detail-wrap .dt-cancel-notice {
margin: 10px 0 0;
text-align: center;
font-size: 14px;
color: #64748b;
}
.detail-wrap .dt-actions {
display: flex;
justify-content: center;
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,110 @@
/**
* 비밀번호 문자열 정책 라이브 검증 (공용)
*
* 서버 검증기 PasswordRuleValidator.isValid(= @PasswordRule) 의
* "문자열" 규칙을 그대로 클라이언트로 포팅한다. 아이디/휴대전화 포함 여부는
* 민감정보 노출을 피하기 위해 서버 검증에만 맡긴다.
*
* 사용법(마크업 구동):
* <ul class="password-policy-checklist" data-password-input="newPassword">
* <li data-rule="length" class="is-idle"><span class="policy-icon"></span><span class="policy-text">...</span></li>
* ... (rule: length | letter | digit | special | nospace | norepeat | noseq)
* </ul>
* <input type="password" id="newPassword" ...>
*
* DOM ready 시 자동으로 스캔하여 대상 input 에 바인딩한다.
*/
(function (global) {
'use strict';
// 3자리 연속(오름/내림) 문자·숫자 검사 — 서버 로직과 동일하게 대문자로 비교
function hasSequential(pw) {
var s = pw.toUpperCase();
for (var i = 0; i < s.length - 2; i++) {
var a = s.charCodeAt(i), b = s.charCodeAt(i + 1), c = s.charCodeAt(i + 2);
var sameCategory = (a > 47 && c < 58) || (a > 64 && c < 91); // 숫자 0-9 또는 영문 A-Z
var consecutive = Math.abs(c - b) === 1 && Math.abs(c - a) === 2;
if (sameCategory && consecutive) {
return true;
}
}
return false;
}
// 규칙별 판정 함수 (통과=true)
var RULES = {
length: function (pw) { return pw.length >= 8 && pw.length <= 50; },
letter: function (pw) { return /[a-zA-Z]/.test(pw); },
digit: function (pw) { return /[0-9]/.test(pw); },
special: function (pw) { return /[^A-Za-z0-9_]/.test(pw); }, // 서버 정규식 \W 기준 (밑줄 제외)
nospace: function (pw) { return !/\s/.test(pw); },
norepeat: function (pw) { return !/(\w)\1\1/.test(pw.toUpperCase()); },
noseq: function (pw) { return !hasSequential(pw); }
};
// 전체 문자열 규칙 통과 여부
function isValid(pw) {
if (!pw) {
return false;
}
for (var key in RULES) {
if (RULES.hasOwnProperty(key) && !RULES[key](pw)) {
return false;
}
}
return true;
}
// 체크리스트(ul) 하나를 대상 input 에 바인딩
function bind(input, list) {
var $input = (input && input.jquery) ? input : $(input);
var $list = (list && list.jquery) ? list : $(list);
var $items = $list.find('li[data-rule]');
if (!$input.length || !$items.length) {
return;
}
function update() {
var pw = $input.val() || '';
$items.each(function () {
var $li = $(this);
var rule = RULES[$li.attr('data-rule')];
if (!rule) {
return;
}
$li.removeClass('is-idle is-pass is-fail');
if (pw.length === 0) {
$li.addClass('is-idle');
} else {
$li.addClass(rule(pw) ? 'is-pass' : 'is-fail');
}
});
}
$input.on('input.passwordPolicy', update);
update();
}
// 마크업 구동 자동 초기화
function init(root) {
var $root = root ? $(root) : $(document);
$root.find('ul.password-policy-checklist[data-password-input]').each(function () {
var $list = $(this);
var input = document.getElementById($list.attr('data-password-input'));
if (input) {
bind(input, $list);
}
});
}
global.PasswordPolicy = {
RULES: RULES,
isValid: isValid,
bind: bind,
init: init
};
$(function () {
init(document);
});
})(window);
@@ -0,0 +1,75 @@
// 비밀번호 문자열 정책 라이브 체크리스트 (마이페이지 비밀번호 변경 + 회원가입 공용)
.password-policy-checklist {
list-style: none;
padding: 0;
margin: 12px 0 0 0;
li {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
line-height: 20px;
margin-bottom: 6px;
transition: color 0.15s ease;
&:last-child {
margin-bottom: 0;
}
.policy-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
flex-shrink: 0;
font-size: 13px;
font-weight: 700;
line-height: 1;
&::before {
content: "\2022"; // •
}
}
&.is-idle {
color: #888;
.policy-icon {
color: #b5b5b5;
}
}
&.is-pass {
color: #1a8f4c;
.policy-icon {
color: #1a8f4c;
&::before {
content: "\2714"; // ✔
}
}
}
&.is-fail {
color: #d63a3a;
.policy-icon {
color: #d63a3a;
&::before {
content: "\2716"; // ✖
}
}
}
}
}
.password-policy-note {
margin: 10px 0 0 0;
font-size: 14px;
line-height: 18px;
color: #888;
}
+1
View File
@@ -44,6 +44,7 @@
@use 'components/breadcrumb' as *;
@use 'components/test-env-notice' as *;
@use 'components/djb-inquiry-comments' as *;
@use 'components/password-policy' as *;
// 5. Page-specific styles
@use 'pages/index' as *;
@@ -1232,6 +1232,14 @@
margin: 0;
}
// 내부 결재 진행중: 신청 취소 불가 안내
.dt-cancel-notice {
margin: 10px 0 0;
text-align: center;
font-size: 14px;
color: #64748b;
}
// Footer Buttons layout
.dt-actions {
display: flex;
@@ -57,8 +57,8 @@
<!-- Status Badge -->
<span class="app-card-badge"
th:classappend="${request.approval != null and request.approval.approvalStatus != null and request.approval.approvalStatus.toString() == 'REQUESTED' ? 'badge-requested' : 'badge-pending'}"
th:text="${request.approval != null and request.approval.approvalStatus != null ? request.approval.approvalStatus.description : '승인대기'}">
승인대기
th:text="${request.approval != null and request.approval.approvalStatus != null ? request.approval.approvalStatus.description : '승인정보 없음'}">
승인정보 없음
</span>
</div>
<!-- App Description & Expected Completion Date -->
@@ -54,6 +54,9 @@
대기중
</span>
</div>
<div class="dt-app-status-row" th:if="${appRequest.approval == null}">
<span class="dt-status-badge status-pending">승인정보 없음</span>
</div>
<h3 class="dt-app-name" th:text="${appRequest.clientName}">앱 이름</h3>
<!-- App Description -->
<p class="dt-app-desc"
@@ -200,12 +203,19 @@
</div>
<!-- 내부 결재 진행중: 취소 불가 안내 -->
<p class="dt-cancel-notice" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
th:if="${appRequest.approval != null and appRequest.approval.approvalStatus != null and
appRequest.approval.approvalStatus.toString() == 'PROCESSING'}">
내부 결재가 진행 중이라 신청을 취소할 수 없습니다. 취소가 필요한 경우 관리자에게 문의해 주세요.
</p>
<!-- Bottom Navigation Actions -->
<div class="dt-actions">
<!-- Cancel Request Button (danger red) -->
<button type="button" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" class="dt-btn-red"
th:if="${appRequest.approval != null and appRequest.approval.approvalStatus != null and
(appRequest.approval.approvalStatus.toString() == 'PENDING' or appRequest.approval.approvalStatus.toString() == 'REQUESTED')}"
th:if="${appRequest.approval == null or (appRequest.approval.approvalStatus != null and
(appRequest.approval.approvalStatus.toString() == 'PENDING' or appRequest.approval.approvalStatus.toString() == 'REQUESTED'))}"
th:data-request-id="${appRequest.id}" onclick="cancelRequestById(this)">
신청 취소
</button>
@@ -51,16 +51,6 @@
<!-- Credential Info Section Card -->
<div class="dt-info-container">
<!-- 인증 방식 -->
<div class="dt-row">
<div class="dt-col">
<label class="dt-label">인증 방식</label>
<div class="dt-input-group">
<div class="dt-input-box" th:text="${authType}">OAuth2</div>
</div>
</div>
</div>
<!-- Client ID -->
<div class="dt-row">
<div class="dt-col">
@@ -15,7 +15,7 @@
<a class="nav-link" th:href="@{/my_company_info}">기업 정보 변경</a>
</li>
<li class="nav-item">
<a class="nav-link active" th:href="@{/change_password}">비밀번호 변경</a>
<a class="nav-link active" th:href="@{/password/verify}">비밀번호 변경</a>
</li>
</ul>
<div class="card mt-2">
@@ -19,7 +19,7 @@
<div class="password-change-wrapper">
<h2 class="page-outer-title">비밀번호 변경</h2>
<form id="passwordChangeForm" th:action="@{/mypage/change_new_password}" method="post">
<form id="passwordChangeForm" th:action="@{/password/change}" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<div class="register-form-container">
@@ -37,7 +37,7 @@
<span class="form-label-text">새 비밀번호</span>
</div>
<div class="form-field-wrapper">
<input type="password" name="newPassword" class="form-input"
<input type="password" id="newPassword" name="newPassword" class="form-input"
placeholder="새 비밀번호 입력" required>
</div>
</div>
@@ -52,10 +52,16 @@
</div>
</div>
<ul class="password-rules-list">
<li>영문 대문자, 소문자, 숫자, 특수문자 8~20자</li>
<li>아이디, 휴대전화, 3자리 이상연속, 반복문자 사용불가</li>
<ul class="password-policy-checklist" data-password-input="newPassword">
<li data-rule="length" class="is-idle"><span class="policy-icon"></span><span class="policy-text">영문/숫자/특수문자 포함 8~50자</span></li>
<li data-rule="letter" class="is-idle"><span class="policy-icon"></span><span class="policy-text">영문 포함</span></li>
<li data-rule="digit" class="is-idle"><span class="policy-icon"></span><span class="policy-text">숫자 포함</span></li>
<li data-rule="special" class="is-idle"><span class="policy-icon"></span><span class="policy-text">특수문자 포함</span></li>
<li data-rule="nospace" class="is-idle"><span class="policy-icon"></span><span class="policy-text">공백 사용 불가</span></li>
<li data-rule="norepeat" class="is-idle"><span class="policy-icon"></span><span class="policy-text">동일 문자 3자리 이상 반복 불가</span></li>
<li data-rule="noseq" class="is-idle"><span class="policy-icon"></span><span class="policy-text">연속된 문자/숫자 3자리 이상 불가</span></li>
</ul>
<p class="password-policy-note">아이디, 휴대전화 번호는 비밀번호에 사용할 수 없습니다.</p>
</div>
<div class="form-actions" style="justify-content: flex-end;">
@@ -16,7 +16,7 @@
<div class="password-change-wrapper">
<h2 class="page-outer-title">비밀번호 변경</h2>
<form th:action="@{/verify_current_password}" method="post">
<form th:action="@{/password/verify}" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<div class="register-form-container">
@@ -42,7 +42,7 @@
<div class="form-actions" style="justify-content: flex-end;">
<div class="right-buttons">
<button type="button" class="btn-cancel" th:onclick="|location.href='@{/}'|">취소</button>
<button type="button" class="btn-cancel" th:unless="${forcedPasswordReset}" th:onclick="|location.href='@{/}'|">취소</button>
<button type="submit" class="btn-apply btn-primary">확인</button>
</div>
</div>
@@ -55,6 +55,26 @@
customPopups.showAlert([[${error}]]);
})
</script>
<script th:if="${forcedPasswordReset}" th:inline="javascript">
// 비밀번호 재설정 강제(ENFORCE): "변경" 또는 "로그아웃"만 선택 가능
$(function () {
$('#customConfirmCloseButton').hide();
$('#customConfirmYesButton').text('변경');
$('#customConfirmNoButton').text('로그아웃');
customPopups.showConfirm(
'계정 보안을 위해 비밀번호 재설정이 필요합니다.<br>비밀번호를 변경하거나 로그아웃해 주세요.',
function (selection) {
if (selection) {
// 변경: 팝업 닫고 현재 페이지(비밀번호 변경)에서 진행
$('#customConfirmCloseButton').show();
} else {
// 로그아웃
window.location.href = [[@{/actionLogout.do}]];
}
}
);
});
</script>
</section>
</body>
</html>
@@ -78,6 +78,16 @@
th:placeholder="#{portalUser.Register.pass}">
<input type="hidden" name="isPasswordValid" id="isPasswordValid"/>
<div id="password-validation" class="org-validation-message"></div>
<ul class="password-policy-checklist" data-password-input="password">
<li data-rule="length" class="is-idle"><span class="policy-icon"></span><span class="policy-text">영문/숫자/특수문자 포함 8~50자</span></li>
<li data-rule="letter" class="is-idle"><span class="policy-icon"></span><span class="policy-text">영문 포함</span></li>
<li data-rule="digit" class="is-idle"><span class="policy-icon"></span><span class="policy-text">숫자 포함</span></li>
<li data-rule="special" class="is-idle"><span class="policy-icon"></span><span class="policy-text">특수문자 포함</span></li>
<li data-rule="nospace" class="is-idle"><span class="policy-icon"></span><span class="policy-text">공백 사용 불가</span></li>
<li data-rule="norepeat" class="is-idle"><span class="policy-icon"></span><span class="policy-text">동일 문자 3자리 이상 반복 불가</span></li>
<li data-rule="noseq" class="is-idle"><span class="policy-icon"></span><span class="policy-text">연속된 문자/숫자 3자리 이상 불가</span></li>
</ul>
<p class="password-policy-note">아이디, 휴대전화 번호는 비밀번호에 사용할 수 없습니다.</p>
</div>
</div>
@@ -107,6 +107,7 @@
<script th:src="@{/plugins/jquery/jquery-3.7.1.min.js}"></script>
<script th:src="@{/js/lodash.js}"></script>
<script th:src="@{/js/common.js}"></script>
<script th:src="@{/js/password-policy.js}"></script>
<script th:src="@{/js/moment.min.js}"></script>
<script th:src="@{/js/daterangepicker.js}"></script>
<script th:src="@{/plugins/codemirror/codemirror.js}"></script>
@@ -97,7 +97,7 @@
<a th:href="@{/statistics/api}"><i class="fas fa-chart-bar"></i>이용 통계</a>
</li>
<li><a th:href="@{/mypage}"><i class="fas fa-user-circle"></i>내 정보 관리</a></li>
<li><a th:href="@{/change_password}"><i class="fas fa-lock"></i>비밀번호 변경</a></li>
<li><a th:href="@{/password/verify}"><i class="fas fa-lock"></i>비밀번호 변경</a></li>
</ul>
</div>
</div>
@@ -253,7 +253,7 @@
<li sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"><a th:href="@{/webhook}">Webhook 관리</a></li>
<li sec:authorize="hasRole('ROLE_APP')"><a th:href="@{/statistics/api}">이용 통계</a></li>
<li><a th:href="@{/mypage}">내 정보 관리</a></li>
<li><a th:href="@{/change_password}">비밀번호 변경</a></li>
<li><a th:href="@{/password/verify}">비밀번호 변경</a></li>
</ul>
</li>
</ul>
@@ -73,7 +73,7 @@
th:classappend="${activeMenu == 'profile'} ? 'service-nav__item--active' : ''"
class="service-nav__item">내정보 관리</a>
<a th:href="@{/change_password}"
<a th:href="@{/password/verify}"
th:classappend="${activeMenu == 'password'} ? 'service-nav__item--active' : ''"
class="service-nav__item">비밀번호 변경</a>
</th:block>