2 Commits

Author SHA1 Message Date
eastargh e69e1a1031 업무오류는 오류건수 집계에서 제외 & 로그조회에서 하늘색 배경색 표시
eapim-admin CI / build (push) Has been cancelled
2026-08-26 13:49:12 +09:00
eastargh 9df39e74bd 취약점 점검 수정 2026-08-26 13:47:32 +09:00
17 changed files with 377 additions and 64 deletions
+1 -1
View File
@@ -175,6 +175,6 @@
</jsp-config> </jsp-config>
<session-config> <session-config>
<session-timeout>60</session-timeout> <session-timeout>10</session-timeout>
</session-config> </session-config>
</web-app> </web-app>
+1 -1
View File
@@ -145,6 +145,6 @@
</jsp-config> </jsp-config>
<session-config> <session-config>
<session-timeout>60</session-timeout> <session-timeout>10</session-timeout>
</session-config> </session-config>
</web-app> </web-app>
+35 -5
View File
@@ -102,7 +102,7 @@
window.location.href = response.redirectUrl; window.location.href = response.redirectUrl;
} else { } else {
// 로그인 실패 // 로그인 실패
alert(response.errorMessage || '로그인에 실패했습니다.'); alert('로그인에 실패했습니다');
} }
}, },
error: function(xhr, status, error) { error: function(xhr, status, error) {
@@ -263,6 +263,35 @@
return true; return true;
} }
// 비밀번호 변경 요청 (AJAX) - 서버 룰 검증 실패 시 모달을 닫지 않고 비밀번호 입력값만 초기화
function submitChangePassword() {
if (!checkPwd()) {
return false;
}
$.ajax({
url: '<c:url value="/changePassword.do"/>',
type: 'POST',
data: $('#modalLoginForm').serialize(),
dataType: 'json',
success: function(response) {
alert(response.message);
if (response.success) {
window.location.href = response.redirectUrl || '<c:url value="/emergency.jsp"/>';
} else {
$("input[name=changePassword]").val('');
$("input[name=confirmPassword]").val('');
$("input[name=changePassword]").trigger("focus");
}
},
error: function() {
alert("<%= localeMessage.getString("login.pwdchangefail") %>");
}
});
return false;
}
$(document).ready(function() { $(document).ready(function() {
$("input[name=userId]").keydown(function(event){ $("input[name=userId]").keydown(function(event){
if ( event.which == 13 ) { if ( event.which == 13 ) {
@@ -276,12 +305,12 @@
}); });
$("input[name=changePassword]").keydown(function(event){ $("input[name=changePassword]").keydown(function(event){
if ( event.which == 13 ) { if ( event.which == 13 ) {
$('#modalLoginForm').submit(); submitChangePassword();
} }
}); });
$("input[name=confirmPassword]").keydown(function(event){ $("input[name=confirmPassword]").keydown(function(event){
if ( event.which == 13 ) { if ( event.which == 13 ) {
$('#modalLoginForm').submit(); submitChangePassword();
} }
}); });
// Password Change 링크로 직접 열 때만 초기화 (JS에서 modal('show') 호출 시 relatedTarget 없음) // Password Change 링크로 직접 열 때만 초기화 (JS에서 modal('show') 호출 시 relatedTarget 없음)
@@ -387,7 +416,7 @@
<span aria-hidden="true">X</span> <span aria-hidden="true">X</span>
</button> </button>
</div> </div>
<form id="modalLoginForm" action="<c:url value="/changePassword.do"/>" method="post" onsubmit="return checkPwd()" novalidate> <form id="modalLoginForm" action="<c:url value="/changePassword.do"/>" method="post" onsubmit="return submitChangePassword()" novalidate>
<div class="modal-body"> <div class="modal-body">
<div id="changeInitPassword"> <div id="changeInitPassword">
<div class="form-group d-flex"> <div class="form-group d-flex">
@@ -405,7 +434,8 @@
<input type="password" name="confirmPassword" class="form-control rounded-left" placeholder="<%= localeMessage.getString("login.placeholderConfirmationPassword") %>" autocomplete="off" required> <input type="password" name="confirmPassword" class="form-control rounded-left" placeholder="<%= localeMessage.getString("login.placeholderConfirmationPassword") %>" autocomplete="off" required>
</div> </div>
<span style="font-size:0.8em; color:red"> <span style="font-size:0.8em; color:red">
※ 7글자 이상 & 영문/숫자/특수문자 2종류 이상 ※ 7글자 이상 & 영문/숫자/특수문자 2종류 이상<br/>
※ 연속된 숫자/문자(예: 123, abc, qwer), 동일 문자 3자 이상 반복 사용 불가
</span> </span>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@@ -24,6 +24,7 @@
var roleString = "<%=SessionManager.getRoleIdString(request)%>"; var roleString = "<%=SessionManager.getRoleIdString(request)%>";
let totalLogs = 0; let totalLogs = 0;
let displayedLogs = 0; let displayedLogs = 0;
var bizErrCodeSet = new Set(); // ERRCODE_BIZ(업무 오류) 코드 목록 - 그리드 배경색 구분용
function adminChk() { function adminChk() {
var arrRoleID = roleString.split(','); var arrRoleID = roleString.split(',');
@@ -106,6 +107,8 @@
new makeOptions("CODE", "NAME").setObj($("select[name=searchExtBizCd]")).setNoValueInclude(true).setNoValue("", "전체").setData(json.extBizList).setFormat(codeName3OptionFormat).rendering(); new makeOptions("CODE", "NAME").setObj($("select[name=searchExtBizCd]")).setNoValueInclude(true).setNoValue("", "전체").setData(json.extBizList).setFormat(codeName3OptionFormat).rendering();
new makeOptions("CODE", "NAME").setObj($("select[name=searchLogPrcssSerno]")).setNoValueInclude(true).setNoValue("", "전체").setData(json.logPrcssSernoList).rendering(); new makeOptions("CODE", "NAME").setObj($("select[name=searchLogPrcssSerno]")).setNoValueInclude(true).setNoValue("", "전체").setData(json.logPrcssSernoList).rendering();
bizErrCodeSet = new Set((json.bizErrCodeList || []).map(function (c) { return c.CODE; }));
setSearchable("searchEaiBzwkDstcd"); // 콤보에 searchable 설정 setSearchable("searchEaiBzwkDstcd"); // 콤보에 searchable 설정
setSearchable("searchExtBizCd"); setSearchable("searchExtBizCd");
@@ -311,7 +314,8 @@
'<%=localeMessage.getString("tracking.companyitcontactname")%>', '<%=localeMessage.getString("tracking.companyitcontactname")%>',
'<%=localeMessage.getString("tracking.companyitcontacttelno")%>', '<%=localeMessage.getString("tracking.companyitcontacttelno")%>',
'<%=localeMessage.getString("tracking.itcontactpointperson1")%>', '<%=localeMessage.getString("tracking.itcontactpointperson1")%>',
'<%=localeMessage.getString("tracking.bzwkcontactperson")%>' '<%=localeMessage.getString("tracking.bzwkcontactperson")%>',
'EAIERRCD'
], ],
colModel: [ colModel: [
{name: 'EAISVCSERNO', hidden: true}, {name: 'EAISVCSERNO', hidden: true},
@@ -341,7 +345,8 @@
{name: 'COMPANYITCONTACTNAME', align: 'left', width: '110', hidden: true}, {name: 'COMPANYITCONTACTNAME', align: 'left', width: '110', hidden: true},
{name: 'COMPANYITCONTACTTELNO', align: 'left', width: '120', hidden: true}, {name: 'COMPANYITCONTACTTELNO', align: 'left', width: '120', hidden: true},
{name: 'ITCONTACTPOINTPERSON1', align: 'left', width: '120', hidden: true}, {name: 'ITCONTACTPOINTPERSON1', align: 'left', width: '120', hidden: true},
{name: 'BZWKCONTACTPERSON', align: 'left', width: '120', hidden: true} {name: 'BZWKCONTACTPERSON', align: 'left', width: '120', hidden: true},
{name: 'EAIERRCD', hidden: true}
], ],
jsonReader: { jsonReader: {
repeatitems: false repeatitems: false
@@ -378,7 +383,7 @@
}, },
loadComplete: function (d) { loadComplete: function (d) {
$("#grid").tuiTableRowSpan("7");
// 거래 건수 표시 // 거래 건수 표시
var uniqueCount = calculateUniqueLogCount("#grid"); var uniqueCount = calculateUniqueLogCount("#grid");
@@ -387,12 +392,20 @@
//에러 행 색상 표시 //에러 행 색상 표시
var rows = $(this).getDataIDs(); var rows = $(this).getDataIDs();
for (var i = 0; i < rows.length; i++) { for (var i = 0; i < rows.length; i++) {
var val = $("#grid").getCell(rows[i], 'ERRYN'); var errYn = $("#grid").getCell(rows[i], 'ERRYN');
if (val == "Y") { var eaiErrCd = $("#grid").getCell(rows[i], 'EAIERRCD');
var isBizErr = eaiErrCd != null && bizErrCodeSet.has(String(eaiErrCd).trim());
if (isBizErr) {
// 업무(BIZ) 오류코드(ERRCODE_BIZ) - 하늘색 배경
$("#" + rows[i]).find("td").css("background-color", "#BEEDFF");
} else if (errYn == "Y") {
$("#" + rows[i]).find("td").css("background-color", "#FFCC99"); $("#" + rows[i]).find("td").css("background-color", "#FFCC99");
} }
} }
$("#grid").tuiTableRowSpan("7");
//1000건이상 alert //1000건이상 alert
if (d.records == 1000) { if (d.records == 1000) {
setTimeout(() => { setTimeout(() => {
@@ -270,6 +270,9 @@ public interface MonitoringContext {
// 최대 로그인 실패 허용횟수 (0이면 계정 잠금 미적용) // 최대 로그인 실패 허용횟수 (0이면 계정 잠금 미적용)
public static final String RMS_PASSWORD_FAIL_COUNT = "rms.password.fail.count"; public static final String RMS_PASSWORD_FAIL_COUNT = "rms.password.fail.count";
// 비밀번호 변경 시 재사용을 금지할 최근 이력 개수
public static final String RMS_PASSWORD_HISTORY_COUNT = "rms.password.history.count";
// API 상태변화 스윙챗 발송여부 // API 상태변화 스윙챗 발송여부
public static final String DJB_UMS_MESSENGER_APIMONITOR_ENABLED = "djb.ums.messenger.api-monitor.enabled"; public static final String DJB_UMS_MESSENGER_APIMONITOR_ENABLED = "djb.ums.messenger.api-monitor.enabled";
@@ -0,0 +1,28 @@
package com.eactive.eai.rms.common.login;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChangePasswordResponseDto {
/**
* 비밀번호 변경 성공 여부
*/
private boolean success;
/**
* 사용자에게 alert로 표시할 메시지
*/
private String message;
/**
* 성공 시 이동할 URL
*/
private String redirectUrl;
}
@@ -47,6 +47,7 @@ import com.eactive.apim.portal.user.entity.UserInfo;
import com.eactive.eai.rms.data.entity.man.user.UserInfoService; import com.eactive.eai.rms.data.entity.man.user.UserInfoService;
import com.eactive.eai.rms.data.entity.man.user.UserLoginHistory; import com.eactive.eai.rms.data.entity.man.user.UserLoginHistory;
import com.eactive.eai.rms.data.entity.man.user.UserLoginHistoryService; import com.eactive.eai.rms.data.entity.man.user.UserLoginHistoryService;
import com.eactive.eai.rms.data.entity.man.user.AdminUserPasswordHistoryService;
import com.eactive.eai.rms.data.entity.man.user.UserRole; import com.eactive.eai.rms.data.entity.man.user.UserRole;
import com.eactive.eai.rms.data.entity.man.user.UserRoleService; import com.eactive.eai.rms.data.entity.man.user.UserRoleService;
import com.eactive.eai.rms.data.entity.man.user.UserServiceTypeService; import com.eactive.eai.rms.data.entity.man.user.UserServiceTypeService;
@@ -62,10 +63,10 @@ public class MainController implements InterceptorSkipController {
private static final String USER_NOT_FOUND_KEY_2 = "login.useridnotfound2"; private static final String USER_NOT_FOUND_KEY_2 = "login.useridnotfound2";
private static final String REDIRECT_LOGIN_URL = "redirect:/emergency.jsp"; private static final String REDIRECT_LOGIN_URL = "redirect:/emergency.jsp";
private static final String REDIRECT_CHANGE_PASSWORD_URL = "redirect:/emergency.jsp";
private static final String USER_STATUS_LOCKED = "2"; private static final String USER_STATUS_LOCKED = "2";
private static final int DEFAULT_MAX_LOGIN_FAIL_COUNT = 5; private static final int DEFAULT_MAX_LOGIN_FAIL_COUNT = 5;
private static final int DEFAULT_PASSWORD_HISTORY_COUNT = 5;
private final LocaleMessage localeMessage; private final LocaleMessage localeMessage;
private final MonitoringContext monitoringContext; private final MonitoringContext monitoringContext;
@@ -76,6 +77,7 @@ public class MainController implements InterceptorSkipController {
private final LoginService loginService; private final LoginService loginService;
private final MonitoringPropertyService monitoringPropertyService; private final MonitoringPropertyService monitoringPropertyService;
private final UserLoginHistoryService userLoginHistoryService; private final UserLoginHistoryService userLoginHistoryService;
private final AdminUserPasswordHistoryService userPasswordHistoryService;
private final SmsAuthService smsAuthService; private final SmsAuthService smsAuthService;
@Autowired @Autowired
@@ -87,6 +89,7 @@ public class MainController implements InterceptorSkipController {
UserServiceTypeService userServiceTypeService, UserServiceTypeService userServiceTypeService,
MonitoringPropertyService monitoringPropertyService, MonitoringPropertyService monitoringPropertyService,
UserLoginHistoryService userLoginHistoryService, UserLoginHistoryService userLoginHistoryService,
AdminUserPasswordHistoryService userPasswordHistoryService,
SmsAuthService smsAuthService) { SmsAuthService smsAuthService) {
this.localeMessage = localeMessage; this.localeMessage = localeMessage;
this.monitoringContext = monitoringContext; this.monitoringContext = monitoringContext;
@@ -96,6 +99,7 @@ public class MainController implements InterceptorSkipController {
this.userServiceTypeService = userServiceTypeService; this.userServiceTypeService = userServiceTypeService;
this.monitoringPropertyService = monitoringPropertyService; this.monitoringPropertyService = monitoringPropertyService;
this.userLoginHistoryService = userLoginHistoryService; this.userLoginHistoryService = userLoginHistoryService;
this.userPasswordHistoryService = userPasswordHistoryService;
this.smsAuthService = smsAuthService; this.smsAuthService = smsAuthService;
} }
@@ -453,6 +457,12 @@ public class MainController implements InterceptorSkipController {
MonitoringContext.RMS_PASSWORD_FAIL_COUNT, DEFAULT_MAX_LOGIN_FAIL_COUNT); MonitoringContext.RMS_PASSWORD_FAIL_COUNT, DEFAULT_MAX_LOGIN_FAIL_COUNT);
} }
// 비밀번호 재사용 금지 대상 최근 이력 개수 조회 (rms.password.history.count)
private int getPasswordHistoryCount() {
return monitoringContext.getIntProperty(
MonitoringContext.RMS_PASSWORD_HISTORY_COUNT, DEFAULT_PASSWORD_HISTORY_COUNT);
}
// 로그인 실패 횟수 증가, 임계치 도달 시 계정 잠금 // 로그인 실패 횟수 증가, 임계치 도달 시 계정 잠금
private int increaseLoginFailCount(UserInfo userInfo) { private int increaseLoginFailCount(UserInfo userInfo) {
int failCount = (userInfo.getLoginfailcount() == null ? 0 : userInfo.getLoginfailcount()) + 1; int failCount = (userInfo.getLoginfailcount() == null ? 0 : userInfo.getLoginfailcount()) + 1;
@@ -824,7 +834,8 @@ public class MainController implements InterceptorSkipController {
} }
@RequestMapping("/changePassword.do") @RequestMapping("/changePassword.do")
public String changePassword(HttpServletRequest request, @ResponseBody
public ChangePasswordResponseDto changePassword(HttpServletRequest request,
String changePassword, String confirmPassword, String changePassword, String confirmPassword,
String resetUserId, String resetPassword) { String resetUserId, String resetPassword) {
HttpSession session = request.getSession(); HttpSession session = request.getSession();
@@ -842,9 +853,10 @@ public class MainController implements InterceptorSkipController {
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F", UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
localeMessage localeMessage
.getString(USER_NOT_FOUND_KEY_1)); .getString(USER_NOT_FOUND_KEY_1));
session.setAttribute(RESULT_MSG, localeMessage return ChangePasswordResponseDto.builder()
.getString(USER_NOT_FOUND_KEY_1)); .success(false)
return REDIRECT_LOGIN_URL; .message(toAlertMessage(localeMessage.getString(USER_NOT_FOUND_KEY_1)))
.build();
} }
UserInfo userInfo = userInfoOptional.get(); UserInfo userInfo = userInfoOptional.get();
@@ -854,9 +866,10 @@ public class MainController implements InterceptorSkipController {
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F", UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
localeMessage localeMessage
.getString("login.pwdmismatch1")); .getString("login.pwdmismatch1"));
session.setAttribute(RESULT_MSG, localeMessage return ChangePasswordResponseDto.builder()
.getString("login.pwdmismatch2")); .success(false)
return REDIRECT_CHANGE_PASSWORD_URL; .message(toAlertMessage(localeMessage.getString("login.pwdmismatch2")))
.build();
} }
// 2. 비밀번호 Validation // 2. 비밀번호 Validation
@@ -864,9 +877,10 @@ public class MainController implements InterceptorSkipController {
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F", UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
localeMessage localeMessage
.getString("login.pwdmismatch3")); .getString("login.pwdmismatch3"));
session.setAttribute(RESULT_MSG, localeMessage return ChangePasswordResponseDto.builder()
.getString("login.pwdmismatch4")); .success(false)
return REDIRECT_CHANGE_PASSWORD_URL; .message(toAlertMessage(localeMessage.getString("login.pwdmismatch4")))
.build();
} }
// 3. 비밀번호 체크 // 3. 비밀번호 체크
@@ -875,37 +889,68 @@ public class MainController implements InterceptorSkipController {
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F", UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
localeMessage localeMessage
.getString("login.pwdmismatch5")); .getString("login.pwdmismatch5"));
session.setAttribute(RESULT_MSG, localeMessage return ChangePasswordResponseDto.builder()
.getString("login.pwdmismatch6")); .success(false)
return REDIRECT_CHANGE_PASSWORD_URL; .message(toAlertMessage(localeMessage.getString("login.pwdmismatch6")))
.build();
} }
// 4. 비밀번호 변경 // 4. 최근 사용한 비밀번호 재사용 금지
userInfo.setPassword(DamoManager.getInstance().hash(DamoManager.SHA256,changePassword)); String newPasswordHash = DamoManager.getInstance().hash(DamoManager.SHA256, changePassword);
if (userPasswordHistoryService.isPasswordReused(resetUserId, newPasswordHash, getPasswordHistoryCount())) {
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
localeMessage
.getString("login.pwdreused1"));
return ChangePasswordResponseDto.builder()
.success(false)
.message(toAlertMessage(localeMessage.getString("login.pwdreused2")))
.build();
}
// 5. 비밀번호 변경
userInfo.setPassword(newPasswordHash);
userInfoService.save(userInfo); userInfoService.save(userInfo);
// 6. 비밀번호 변경 이력 저장 (재사용 금지 검증용)
userPasswordHistoryService.recordPasswordChange(resetUserId, newPasswordHash);
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "S", localeMessage UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "S", localeMessage
.getString("login.pwdchanged")); .getString("login.pwdchanged"));
session.setAttribute(RESULT_MSG, localeMessage
.getString("login.pwdchanged")); return ChangePasswordResponseDto.builder()
logger.debug("\n\n\n\n===================" + localeMessage .success(true)
.getString("login.pwdchanged")); .message(toAlertMessage(localeMessage.getString("login.pwdchanged")))
return REDIRECT_LOGIN_URL; .redirectUrl(request.getContextPath() + "/emergency.jsp")
.build();
} catch (Exception e) { } catch (Exception e) {
logger.error("changePassword Error ", e); logger.error("changePassword Error ", e);
return "redirect:/common/errors/errorLogon.jsp"; return ChangePasswordResponseDto.builder()
.success(false)
.message(toAlertMessage(localeMessage.getString("login.pwdchangefail")))
.build();
} }
} }
private static final int PASSWORD_PATTERN_CHECK_LENGTH = 3;
private static final String[] KEYBOARD_SEQUENTIAL_ROWS = {
"1234567890",
"!@#$%^&*()",
"qwertyuiop",
"asdfghjkl",
"zxcvbnm"
};
private boolean isPasswordValid(String password) { private boolean isPasswordValid(String password) {
return password != null && password.getBytes().length == password return password != null && password.getBytes().length == password
.length() && password.getBytes().length >= 7 .length() && password.getBytes().length >= 7
&& isCombination(password) && isCombination(password)
&& !isRepeatPassword(password, 5) && !isRepeatPassword(password, PASSWORD_PATTERN_CHECK_LENGTH)
&& !isSerialPassword(password, 5); && !isSerialPassword(password, PASSWORD_PATTERN_CHECK_LENGTH)
&& !isKeyboardSequentialPassword(password, PASSWORD_PATTERN_CHECK_LENGTH);
} }
private boolean isRepeatPassword(String password, int count) { private boolean isRepeatPassword(String password, int count) {
@@ -930,21 +975,41 @@ public class MainController implements InterceptorSkipController {
} }
private boolean isSerialPassword(String password, int count) { private boolean isSerialPassword(String password, int count) {
int preData = 0; int ascCount = 1;
int serialcount = 1; int descCount = 1;
for (int i = 0; i < password.length(); i++) { for (int i = 1; i < password.length(); i++) {
char prev = password.charAt(i - 1);
char curr = password.charAt(i);
ascCount = (curr == prev + 1) ? ascCount + 1 : 1;
descCount = (curr == prev - 1) ? descCount + 1 : 1;
if (ascCount >= count || descCount >= count) { return true; }
}
return false;
}
// 키보드 자판 배열상 연속된 문자열(qwer, asdf 등, 역순 포함) 사용 여부 체크
private boolean isKeyboardSequentialPassword(String password, int count) {
String lowerPassword = password.toLowerCase();
for (String row : KEYBOARD_SEQUENTIAL_ROWS) {
for (int i = 0; i <= row.length() - count; i++) {
String forward = row.substring(i, i + count);
String backward = new StringBuilder(forward).reverse().toString();
if (lowerPassword.contains(forward) || lowerPassword.contains(backward)) {
return true;
}
if (preData + 1 == password.charAt(i)) {
serialcount++;
} else {
serialcount = 1;
} }
preData = password.charAt(i);
if (serialcount == count) { return true; }
} }
return false; return false;
@@ -0,0 +1,77 @@
package com.eactive.eai.rms.data.entity.man.user;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import org.hibernate.annotations.Comment;
import org.hibernate.annotations.GenericGenerator;
import com.eactive.eai.data.entity.AbstractEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
/**
* 관리콘솔(TSEAIRM02) 로그인 사용자의 비밀번호 변경 이력 (TSEAIRM08).
* 포털 API 소비자 비밀번호 이력({@code com.eactive.apim.portal.portaluser.entity.UserPasswordHistory})과는 별개 테이블/도메인이다.
*/
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true)
@Entity
@Table(name = "TSEAIRM08")
@org.hibernate.annotations.Table(appliesTo = "TSEAIRM08", comment = "관리콘솔 사용자 비밀번호 변경 이력")
public class AdminUserPasswordHistory extends AbstractEntity<Integer> implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GenericGenerator(
name = "TSEAIRM08_GEN",
strategy = "com.eactive.eai.rms.data.jpa.SchemaPrefixedSequenceGenerator",
parameters = {
@org.hibernate.annotations.Parameter(name = "sequence_name", value = "SEQ_TSEAIRM08"),
@org.hibernate.annotations.Parameter(name = "initial_value", value = "1"),
@org.hibernate.annotations.Parameter(name = "increment_size", value = "1")
}
)
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "TSEAIRM08_GEN"
)
@Column(name = "ID", unique = true, nullable = false)
@Comment("비밀번호 변경 이력 식별 ID")
private int pwdHistId;
@Column(name = "USERID", length = 255, nullable = false)
@Comment("사용자 식별자(사번)")
private String userId;
@Column(name = "PASSWORD_HASH", length = 255, nullable = false)
@Comment("변경된 비밀번호 해시값")
private String passwordHash;
@Column(name = "CHANGE_DATE", length = 14)
@Comment("비밀번호 변경 일시 (yyyyMMddHHmmss)")
private String changeDate;
@Column(name = "CREATED_BY", length = 200)
@Comment("등록자 식별자")
private String createdBy;
@Column(name = "CREATED_DATE", length = 14)
@Comment("레코드 생성 일시 (yyyyMMddHHmmss)")
private String createdDate;
@Override
public @NonNull Integer getId() {
return pwdHistId;
}
}
@@ -0,0 +1,9 @@
package com.eactive.eai.rms.data.entity.man.user;
import com.eactive.eai.rms.data.EMSDataSource;
import com.eactive.eai.data.jpa.BaseRepository;
@EMSDataSource
public interface AdminUserPasswordHistoryRepository extends BaseRepository<AdminUserPasswordHistory, Integer> {
}
@@ -0,0 +1,53 @@
package com.eactive.eai.rms.data.entity.man.user;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.data.converter.LocalDateTimeFormatters;
import com.eactive.eai.rms.data.jpa.AbstractEMSDataSerivce;
import com.querydsl.core.types.dsl.BooleanExpression;
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
public class AdminUserPasswordHistoryService
extends AbstractEMSDataSerivce<AdminUserPasswordHistory, Integer, AdminUserPasswordHistoryRepository> {
/** 사용자의 최근 비밀번호 이력을 최신순으로 historyCount건 조회 */
public List<AdminUserPasswordHistory> findRecent(String userId, int historyCount) {
BooleanExpression predicate = QAdminUserPasswordHistory.adminUserPasswordHistory.userId.eq(userId);
Pageable pageable = PageRequest.of(0, historyCount, Sort.by(Sort.Direction.DESC, "pwdHistId"));
return repository.findAll(predicate, pageable).getContent();
}
/** 최근 historyCount건 이내에 동일한 비밀번호(해시값)가 있었는지 확인 */
public boolean isPasswordReused(String userId, String newPasswordHash, int historyCount) {
if (historyCount <= 0) {
return false;
}
return findRecent(userId, historyCount).stream()
.anyMatch(history -> history.getPasswordHash().equals(newPasswordHash));
}
/** 비밀번호 변경 이력 저장 */
public void recordPasswordChange(String userId, String passwordHash) {
String now = LocalDateTime.now().format(LocalDateTimeFormatters.FORMATTER_YYYYMMDDHHMMSS_14);
AdminUserPasswordHistory history = new AdminUserPasswordHistory();
history.setUserId(userId);
history.setPasswordHash(passwordHash);
history.setChangeDate(now);
history.setCreatedBy(userId);
history.setCreatedDate(now);
save(history);
}
}
@@ -19,7 +19,8 @@ public class EAILogDto {
public EAILogDto(EAILogId eaiLogId, String trackasiskey1ctnt, String trackasiskey2ctnt, String keymgtmsgctnt, public EAILogDto(EAILogId eaiLogId, String trackasiskey1ctnt, String trackasiskey2ctnt, String keymgtmsgctnt,
String eaisvcname, String extbizcd, String refkey, String clientname, String eaisvcname, String extbizcd, String refkey, String clientname,
String orgname, String msgprcssyms, String companycode, String eaierrctnt, String eaiSvcDesc) { String orgname, String msgprcssyms, String companycode, String eaierrctnt,
String eaierrcd, String eaiSvcDesc) {
this.eaiLog = new EAILog(); this.eaiLog = new EAILog();
this.eaiLog.setId(eaiLogId); this.eaiLog.setId(eaiLogId);
this.eaiLog.setTrackasiskey1ctnt(trackasiskey1ctnt); this.eaiLog.setTrackasiskey1ctnt(trackasiskey1ctnt);
@@ -33,6 +34,7 @@ public class EAILogDto {
this.eaiLog.setMsgprcssyms(msgprcssyms); this.eaiLog.setMsgprcssyms(msgprcssyms);
this.eaiLog.setCompanycode(companycode); this.eaiLog.setCompanycode(companycode);
this.eaiLog.setEaierrctnt(eaierrctnt); this.eaiLog.setEaierrctnt(eaierrctnt);
this.eaiLog.setEaierrcd(eaierrcd);
this.eaiSvcDesc = eaiSvcDesc; this.eaiSvcDesc = eaiSvcDesc;
} }
} }
@@ -5,6 +5,7 @@ import com.eactive.eai.data.entity.onl.logger.EAILogId;
import com.eactive.eai.data.entity.onl.logger.QEAILog; import com.eactive.eai.data.entity.onl.logger.QEAILog;
import com.eactive.eai.data.entity.onl.message.QEAIMessageEntity; import com.eactive.eai.data.entity.onl.message.QEAIMessageEntity;
import com.eactive.eai.rms.data.entity.onl.bzwkdstcd.QUserBusiness; import com.eactive.eai.rms.data.entity.onl.bzwkdstcd.QUserBusiness;
import com.eactive.eai.rms.data.entity.onl.code.CodeService;
import com.querydsl.core.Tuple; import com.querydsl.core.Tuple;
import com.querydsl.core.types.Projections; import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.BooleanExpression;
@@ -12,6 +13,7 @@ import com.querydsl.core.types.dsl.EntityPathBase;
import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory; import com.querydsl.jpa.impl.JPAQueryFactory;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
@@ -27,8 +29,15 @@ import java.util.stream.Collectors;
@Repository @Repository
public class EAILogRepositoryImpl implements EAILogRepository { public class EAILogRepositoryImpl implements EAILogRepository {
// 업무(BIZ) 오류코드 그룹 - 해당 코드는 시스템 에러가 아닌 정상적인 업무 거부 응답이므로
// "에러로그" 검색 조건에서는 제외한다.
private static final String ERRCODE_BIZ_GROUP = "ERRCODE_BIZ";
private EntityManager em; private EntityManager em;
@Autowired
private CodeService codeService;
@PersistenceContext @PersistenceContext
public void setEm(EntityManager em) { public void setEm(EntityManager em) {
this.em = em; this.em = em;
@@ -57,7 +66,7 @@ public class EAILogRepositoryImpl implements EAILogRepository {
qeaiLog.id, qeaiLog.trackasiskey1ctnt, qeaiLog.trackasiskey2ctnt, qeaiLog.keymgtmsgctnt, qeaiLog.id, qeaiLog.trackasiskey1ctnt, qeaiLog.trackasiskey2ctnt, qeaiLog.keymgtmsgctnt,
qeaiLog.eaisvcname, qeaiLog.extbizcd, qeaiLog.refkey, qeaiLog.clientname, qeaiLog.eaisvcname, qeaiLog.extbizcd, qeaiLog.refkey, qeaiLog.clientname,
qeaiLog.orgname, qeaiLog.msgprcssyms, qeaiLog.companycode, qeaiLog.eaierrctnt, qeaiLog.orgname, qeaiLog.msgprcssyms, qeaiLog.companycode, qeaiLog.eaierrctnt,
qeaiMessageEntity.eaisvcdesc)) qeaiLog.eaierrcd, qeaiMessageEntity.eaisvcdesc))
.from(entityPathBase) .from(entityPathBase)
.leftJoin(qeaiMessageEntity) .leftJoin(qeaiMessageEntity)
.on(qeaiLog.eaisvcname.eq(qeaiMessageEntity.eaisvcname)) .on(qeaiLog.eaisvcname.eq(qeaiMessageEntity.eaisvcname))
@@ -195,8 +204,18 @@ public class EAILogRepositoryImpl implements EAILogRepository {
.between(eaiLogSearch.getSearchStartTime(), eaiLogSearch.getSearchEndTime())) .between(eaiLogSearch.getSearchStartTime(), eaiLogSearch.getSearchEndTime()))
.where( .where(
qeaiLog.eaierrcd.trim().isNotEmpty() qeaiLog.eaierrcd.trim().isNotEmpty()
) );
.distinct();
// ERRCODE_BIZ(업무 오류) 코드는 시스템 에러가 아니므로 "에러로그" 검색 대상에서 제외
List<String> bizErrCodes = codeService.findByIdCodegroup(ERRCODE_BIZ_GROUP).stream()
.map(code -> code.getId().getCode())
.collect(Collectors.toList());
if (!bizErrCodes.isEmpty()) {
errorInstancesSubQuery.where(qeaiLog.eaierrcd.notIn(bizErrCodes));
}
errorInstancesSubQuery.distinct();
// 찾은 eaisevrinstncname에 해당하는 모든 로그 조회 // 찾은 eaisevrinstncname에 해당하는 모든 로그 조회
query.where(qeaiLog.id.eaisvcserno.in(errorInstancesSubQuery)); query.where(qeaiLog.id.eaisvcserno.in(errorInstancesSubQuery));
@@ -126,9 +126,9 @@ public class ApiStatsHourlyAggregationJob implements Job {
" , API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, NVL(A.CLIENT_ID, 'NONE')" + " , API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, NVL(A.CLIENT_ID, 'NONE')" +
" , INBOUND_ADAPTER, OUTBOUND_ADAPTER" + " , INBOUND_ADAPTER, OUTBOUND_ADAPTER" +
" , COUNT(EAISVCSERNO)" + " , COUNT(EAISVCSERNO)" +
" , SUM(CASE WHEN A.ERROR_CODE IS NULL AND E400 IS NOT NULL THEN 1 ELSE 0 END)" + " , SUM(CASE WHEN (A.ERROR_CODE IS NULL OR C.CODE IS NOT NULL) AND E400 IS NOT NULL THEN 1 ELSE 0 END)" +
" , SUM(CASE WHEN B.CODE IS NOT NULL THEN 1 ELSE 0 END)" + " , SUM(CASE WHEN B.CODE IS NOT NULL THEN 1 ELSE 0 END)" +
" , SUM(CASE WHEN (A.E400 IS NULL OR A.ERROR_CODE IS NOT NULL) AND B.CODE IS NULL THEN 1 ELSE 0 END)" + " , SUM(CASE WHEN (A.E400 IS NULL OR A.ERROR_CODE IS NOT NULL) AND B.CODE IS NULL AND C.CODE IS NULL THEN 1 ELSE 0 END)" +
" , 0, 0, 0, 0" + " , 0, 0, 0, 0" +
" , TRUNC(AVG(RESP_TIME)), MIN(RESP_TIME), MAX(RESP_TIME), 0, 0" + " , TRUNC(AVG(RESP_TIME)), MIN(RESP_TIME), MAX(RESP_TIME), 0, 0" +
" FROM (" + " FROM (" +
@@ -145,10 +145,11 @@ public class ApiStatsHourlyAggregationJob implements Job {
" ) AS RESP_TIME" + " ) AS RESP_TIME" +
" , MAX(CASE WHEN LOGPRCSSSERNO = '400' THEN LOGPRCSSSERNO ELSE '' END) AS E400" + " , MAX(CASE WHEN LOGPRCSSSERNO = '400' THEN LOGPRCSSSERNO ELSE '' END) AS E400" +
" , MAX(EAIERRCD) AS ERROR_CODE" + " , MAX(EAIERRCD) AS ERROR_CODE" +
" FROM " + logTableName + " FROM " + logTableName + " A " +
" WHERE MSGDPSTYMS >= :rangeStart AND MSGDPSTYMS < :rangeEnd" + " WHERE MSGDPSTYMS >= :rangeStart AND MSGDPSTYMS < :rangeEnd" +
" GROUP BY EAISVCSERNO" + " GROUP BY EAISVCSERNO" +
" ) A LEFT OUTER JOIN TSEAICM20 B ON A.ERROR_CODE = B.CODE AND B.CODEGROUP = 'ERRCODE_TIMEOUT' AND B.USEYN = 'Y' " + " ) A LEFT OUTER JOIN TSEAICM20 B ON A.ERROR_CODE = B.CODE AND B.CODEGROUP = 'ERRCODE_TIMEOUT' AND B.USEYN = 'Y' " +
" LEFT OUTER JOIN TSEAICM20 C ON A.ERROR_CODE = C.CODE AND C.CODEGROUP = 'ERRCODE_BIZ' AND C.USEYN = 'Y' " +
" GROUP BY SUBSTR(DT,1,10), API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, NVL(A.CLIENT_ID, 'NONE')" + " GROUP BY SUBSTR(DT,1,10), API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, NVL(A.CLIENT_ID, 'NONE')" +
" , INBOUND_ADAPTER, OUTBOUND_ADAPTER"; " , INBOUND_ADAPTER, OUTBOUND_ADAPTER";
@@ -426,10 +426,14 @@ public class DbTrackingService extends BaseService {
List<ComboVo> listLogPrcssSerno = comboService List<ComboVo> listLogPrcssSerno = comboService
.getFromCode("LOGPRCSSSERNO_TYPE"); .getFromCode("LOGPRCSSSERNO_TYPE");
// 업무(BIZ) 오류코드 목록 - 그리드에서 별도 배경색으로 표시하기 위함
List<ComboVo> bizErrCodeList = comboService.getFromCode("ERRCODE_BIZ");
Map<String, List<ComboVo>> map = new HashMap<>(); Map<String, List<ComboVo>> map = new HashMap<>();
map.put("eaiBizList", bizList); map.put("eaiBizList", bizList);
map.put("extBizList", extBizList); map.put("extBizList", extBizList);
map.put("logPrcssSernoList", listLogPrcssSerno); map.put("logPrcssSernoList", listLogPrcssSerno);
map.put("bizErrCodeList", bizErrCodeList);
return map; return map;
} }
@@ -1215,9 +1215,12 @@ login.pwdchanged = Your password has been changed.
login.pwdmismatch1 = Your change password is different from your verification password. login.pwdmismatch1 = Your change password is different from your verification password.
login.pwdmismatch2 = Your change password is different from your verification password. <BR/> Please check your login information. login.pwdmismatch2 = Your change password is different from your verification password. <BR/> Please check your login information.
login.pwdmismatch3 = The change password does not match the rule. login.pwdmismatch3 = The change password does not match the rule.
login.pwdmismatch4 = The change password does not match the rule. <BR/> Please check your login information. login.pwdmismatch4 = The change password does not match the rule.
login.pwdmismatch5 = Your password is different. login.pwdmismatch5 = Your password is different.
login.pwdmismatch6 = Your password is different. <BR/> Please check your login information. login.pwdmismatch6 = Your password is different.
login.pwdreused1 = You cannot reuse a recently used password.
login.pwdreused2 = You cannot reuse a recently used password. <BR/> Please enter a different password.
login.pwdchangefail = An error occurred while changing the password.
login.pwdnotchanged1 = You did not change your password after resetting your password. Please change your password and login. login.pwdnotchanged1 = You did not change your password after resetting your password. Please change your password and login.
login.pwdnotchanged2 = You did not change your password after resetting your password.<BR/>Please change your password and login. login.pwdnotchanged2 = You did not change your password after resetting your password.<BR/>Please change your password and login.
login.seedencriptfail = Seed encryption failed. <BR/> Please sign in again. login.seedencriptfail = Seed encryption failed. <BR/> Please sign in again.
@@ -1360,9 +1360,12 @@ login.pwdchanged = Your password has been changed.
login.pwdmismatch1 = Your change password is different from your verification password. login.pwdmismatch1 = Your change password is different from your verification password.
login.pwdmismatch2 = Your change password is different from your verification password. <BR/> Please check your login information. login.pwdmismatch2 = Your change password is different from your verification password. <BR/> Please check your login information.
login.pwdmismatch3 = The change password does not match the rule. login.pwdmismatch3 = The change password does not match the rule.
login.pwdmismatch4 = The change password does not match the rule. <BR/> Please check your login information. login.pwdmismatch4 = The change password does not match the rule.
login.pwdmismatch5 = Your password is different. login.pwdmismatch5 = Your password is different.
login.pwdmismatch6 = Your password is different. <BR/> Please check your login information. login.pwdmismatch6 = Your password is different.
login.pwdreused1 = You cannot reuse a recently used password.
login.pwdreused2 = You cannot reuse a recently used password. <BR/> Please enter a different password.
login.pwdchangefail = An error occurred while changing the password.
login.pwdnotchanged1 = You did not change your password after resetting your password. Please change your password and login. login.pwdnotchanged1 = You did not change your password after resetting your password. Please change your password and login.
login.pwdnotchanged2 = You did not change your password after resetting your password.<BR/>Please change your password and login. login.pwdnotchanged2 = You did not change your password after resetting your password.<BR/>Please change your password and login.
login.seedencriptfail = Seed encryption failed. <BR/> Please sign in again. login.seedencriptfail = Seed encryption failed. <BR/> Please sign in again.
@@ -1440,9 +1440,12 @@ login.pwdchanged = \uBE44\uBC00\uBC88\uD638\uAC00 \uBCC0\uA
login.pwdmismatch1 = \uBCC0\uACBD \uBE44\uBC00\uBC88\uD638 \uC640 \uD655\uC778 \uBE44\uBC00\uBC88\uD638\uAC00 \uB2E4\uB985\uB2C8\uB2E4. login.pwdmismatch1 = \uBCC0\uACBD \uBE44\uBC00\uBC88\uD638 \uC640 \uD655\uC778 \uBE44\uBC00\uBC88\uD638\uAC00 \uB2E4\uB985\uB2C8\uB2E4.
login.pwdmismatch2 = \uBCC0\uACBD \uBE44\uBC00\uBC88\uD638 \uC640 \uD655\uC778 \uBE44\uBC00\uBC88\uD638\uAC00 \uB2E4\uB985\uB2C8\uB2E4.<BR/> \uB85C\uADF8\uC778\uC815\uBCF4\uB97C \uD655\uC778\uD558\uC138\uC694. login.pwdmismatch2 = \uBCC0\uACBD \uBE44\uBC00\uBC88\uD638 \uC640 \uD655\uC778 \uBE44\uBC00\uBC88\uD638\uAC00 \uB2E4\uB985\uB2C8\uB2E4.<BR/> \uB85C\uADF8\uC778\uC815\uBCF4\uB97C \uD655\uC778\uD558\uC138\uC694.
login.pwdmismatch3 = \uBCC0\uACBD\uBE44\uBC00\uBC88\uD638\uAC00 \uB8F0\uC5D0 \uC548\uB9DE\uC2B5\uB2C8\uB2E4. login.pwdmismatch3 = \uBCC0\uACBD\uBE44\uBC00\uBC88\uD638\uAC00 \uB8F0\uC5D0 \uC548\uB9DE\uC2B5\uB2C8\uB2E4.
login.pwdmismatch4 = \uBCC0\uACBD\uBE44\uBC00\uBC88\uD638\uAC00 \uB8F0\uC5D0 \uC548\uB9DE\uC2B5\uB2C8\uB2E4.<BR/> \uB85C\uADF8\uC778\uC815\uBCF4\uB97C \uD655\uC778\uD558\uC138\uC694. login.pwdmismatch4 = \uBCC0\uACBD\uBE44\uBC00\uBC88\uD638\uAC00 \uB8F0\uC5D0 \uC548\uB9DE\uC2B5\uB2C8\uB2E4.
login.pwdmismatch5 = \uBCC0\uACBD\uC804 \uBE44\uBC00\uBC88\uD638\uAC00 \uB2E4\uB985\uB2C8\uB2E4. login.pwdmismatch5 = \uBCC0\uACBD\uC804 \uBE44\uBC00\uBC88\uD638\uAC00 \uB2E4\uB985\uB2C8\uB2E4.
login.pwdmismatch6 = \uBCC0\uACBD\uC804 \uBE44\uBC00\uBC88\uD638\uAC00 \uB2E4\uB985\uB2C8\uB2E4.<BR/> \uB85C\uADF8\uC778\uC815\uBCF4\uB97C \uD655\uC778\uD558\uC138\uC694. login.pwdmismatch6 = \uBCC0\uACBD\uC804 \uBE44\uBC00\uBC88\uD638\uAC00 \uB2E4\uB985\uB2C8\uB2E4.
login.pwdreused1 = \uCD5C\uADFC \uC0AC\uC6A9\uD55C \uBE44\uBC00\uBC88\uD638\uB294 \uB2E4\uC2DC \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.
login.pwdreused2 = \uCD5C\uADFC \uC0AC\uC6A9\uD55C \uBE44\uBC00\uBC88\uD638\uB294 \uB2E4\uC2DC \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.<BR/> \uB2E4\uB978 \uBE44\uBC00\uBC88\uD638\uB97C \uC785\uB825\uD558\uC138\uC694.
login.pwdchangefail = \uBE44\uBC00\uBC88\uD638 \uBCC0\uACBD \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.
login.pwdnotchanged1 = \uBE44\uBC00\uBC88\uD638 \uCD08\uAE30\uD654\uD6C4 \uBE44\uBC00\uBC88\uD638 \uBCC0\uACBD\uC744 \uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.\uBE44\uBC00\uBC88\uD638\uB97C \uBCC0\uACBD\uD558\uC2DC\uACE0 \uB85C\uADF8\uC778 \uD558\uC138\uC694 login.pwdnotchanged1 = \uBE44\uBC00\uBC88\uD638 \uCD08\uAE30\uD654\uD6C4 \uBE44\uBC00\uBC88\uD638 \uBCC0\uACBD\uC744 \uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.\uBE44\uBC00\uBC88\uD638\uB97C \uBCC0\uACBD\uD558\uC2DC\uACE0 \uB85C\uADF8\uC778 \uD558\uC138\uC694
login.pwdnotchanged2 = \uBE44\uBC00\uBC88\uD638 \uCD08\uAE30\uD654\uD6C4 \uBE44\uBC00\uBC88\uD638 \uBCC0\uACBD\uC744 \uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.\uBE44\uBC00\uBC88\uD638\uB97C \uBCC0\uACBD\uD558\uC2DC\uACE0 \uB85C\uADF8\uC778 \uD558\uC138\uC694.<BR/> \uB85C\uADF8\uC778\uC815\uBCF4\uB97C \uD655\uC778\uD558\uC138\uC694. login.pwdnotchanged2 = \uBE44\uBC00\uBC88\uD638 \uCD08\uAE30\uD654\uD6C4 \uBE44\uBC00\uBC88\uD638 \uBCC0\uACBD\uC744 \uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.\uBE44\uBC00\uBC88\uD638\uB97C \uBCC0\uACBD\uD558\uC2DC\uACE0 \uB85C\uADF8\uC778 \uD558\uC138\uC694.<BR/> \uB85C\uADF8\uC778\uC815\uBCF4\uB97C \uD655\uC778\uD558\uC138\uC694.
login.seedencriptfail = Seed \uC554\uD638\uD654\uC5D0 \uC2E4\uD328 \uD558\uC600\uC2B5\uB2C8\uB2E4.<BR/> \uB2E4\uC2DC \uB85C\uADF8\uC778\uD558\uC138\uC694. login.seedencriptfail = Seed \uC554\uD638\uD654\uC5D0 \uC2E4\uD328 \uD558\uC600\uC2B5\uB2C8\uB2E4.<BR/> \uB2E4\uC2DC \uB85C\uADF8\uC778\uD558\uC138\uC694.