Merge remote-tracking branch 'origin/devs/ums' into main-design-layout

# Conflicts:
#	src/main/resources/templates/views/apps/login/login.html
This commit is contained in:
현성필
2025-11-14 11:13:46 +09:00
46 changed files with 2258 additions and 426 deletions
@@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import java.security.SecureRandom;
@Component
@@ -18,6 +19,12 @@ public class AuthNumberGenerator {
}
@PostConstruct
public void init() {
log.warn("가상 인증코드 활성화 상태 - {}", portalProperties.getAuthVirtualCode());
}
public String generateAuthNumber() {
if (StringUtils.hasLength(portalProperties.getAuthVirtualCode())) {
log.info("가상 인증코드 활성화 상태 - {}", portalProperties.getAuthVirtualCode());
@@ -22,10 +22,10 @@ public class AuthNumberServiceImpl implements AuthNumberService {
private final AuthNumberGenerator generator;
private final MessageSender messageSender;
@Value("${auth-ttl:300}")
@Value("${portal.auth-ttl:300}")
private int authNumberExpirationTime;
@Value("${auth.resend.limit.seconds:30}")
@Value("${portal.auth.resend_limit_seconds:30}")
private int resendLimitSeconds;
@Autowired
@@ -39,7 +39,6 @@ public class MessageSender {
} else {
throw new IllegalArgumentException("Unsupported message type: " + msgType);
}
messageHandlerService.publishEvent(code, recipient, params);
}
}
@@ -46,6 +46,7 @@ public class AccountController {
private final UserFacade userFacade;
private final OrgRegisterFacade orgRegisterFacade;
private final AgreementsFacade agreementsFacade;
private final com.eactive.apim.portal.apps.user.facade.AuthFacade authFacade;
@PostMapping("/confirm_password")
@@ -326,4 +327,115 @@ public class AccountController {
return "redirect:/mypage";
}
}
@GetMapping("/mypage/verification-email")
public String showVerificationEmailPage(Model model) {
try {
// 현재 로그인한 사용자 정보 가져오기
PortalAuthenticatedUser currentUser = SecurityUtil.getPortalAuthenticatedUser();
if (currentUser == null) {
return "redirect:/login";
}
// 사용자 이메일 주소를 모델에 추가
model.addAttribute("email", currentUser.getUsername());
model.addAttribute("userId", currentUser.getId());
return "apps/mypage/verificationEmail";
} catch (Exception e) {
logger.error("이메일 인증 페이지 로드 실패", e);
return "redirect:/";
}
}
@PostMapping("/mypage/send-verification-code")
public ResponseEntity<ValidationResponse> sendVerificationCode(
@org.springframework.web.bind.annotation.RequestBody java.util.Map<String, String> request,
HttpSession session) {
try {
String email = request.get("email");
// 현재 로그인한 사용자의 이메일과 일치하는지 확인
PortalAuthenticatedUser currentUser = SecurityUtil.getPortalAuthenticatedUser();
if (currentUser == null || !currentUser.getUsername().equals(email)) {
ValidationResponse response = new ValidationResponse();
response.setValid(false);
response.setMessage("유효하지 않은 요청입니다.");
return ResponseEntity.badRequest().body(response);
}
// 이전 세션 데이터 정리
session.removeAttribute("emailVerified");
// 이메일 인증 코드 발송
ValidationResponse response = authFacade.requestAuth(email, "EMAIL");
if (response.isValid()) {
session.setAttribute("verificationEmail", email);
}
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("이메일 인증 코드 발송 실패", e);
ValidationResponse response = new ValidationResponse();
response.setValid(false);
response.setMessage("인증 코드 발송에 실패했습니다.");
return ResponseEntity.status(500).body(response);
}
}
@PostMapping("/mypage/verify-email-code")
public ResponseEntity<ValidationResponse> verifyEmailCode(
@org.springframework.web.bind.annotation.RequestBody java.util.Map<String, String> request,
HttpSession session) {
try {
String email = request.get("email");
String code = request.get("code");
// 현재 로그인한 사용자의 이메일과 일치하는지 확인
PortalAuthenticatedUser currentUser = SecurityUtil.getPortalAuthenticatedUser();
if (currentUser == null || !currentUser.getUsername().equals(email)) {
ValidationResponse response = new ValidationResponse();
response.setValid(false);
response.setMessage("유효하지 않은 요청입니다.");
return ResponseEntity.badRequest().body(response);
}
// 세션에 저장된 이메일과 일치하는지 확인
String sessionEmail = (String) session.getAttribute("verificationEmail");
if (sessionEmail == null || !sessionEmail.equals(email)) {
ValidationResponse response = new ValidationResponse();
response.setValid(false);
response.setMessage("인증 요청된 이메일과 일치하지 않습니다.");
return ResponseEntity.badRequest().body(response);
}
// 인증 코드 확인
ValidationResponse response = authFacade.verifyAuthNumber(email, code);
if (response.isValid()) {
session.setAttribute("emailVerified", true);
// 사용자 상태를 ACTIVE로 변경
userFacade.activateUserByEmail(email);
// 이메일 인증 관련 세션 정보 제거
session.removeAttribute("emailVerificationRequired");
session.removeAttribute("redirectUrl");
logger.info("이메일 인증 완료: {}", email);
}
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("이메일 인증 코드 확인 실패", e);
ValidationResponse response = new ValidationResponse();
response.setValid(false);
response.setMessage("인증 코드 확인에 실패했습니다.");
return ResponseEntity.status(500).body(response);
}
}
}
@@ -3,8 +3,7 @@ package com.eactive.apim.portal.apps.user.dto;
import com.eactive.apim.portal.common.validator.AuthNumberMatch;
import com.eactive.apim.portal.common.validator.CellPhone;
import com.eactive.apim.portal.common.validator.PasswordMatch;
import com.eactive.apim.portal.common.validator.PasswordRule;
import com.eactive.apim.portal.common.validator.PasswordRuleForKbank;
import com.eactive.apim.portal.common.validator.PasswordRuleForKjbank;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotEmpty;
@@ -13,7 +12,7 @@ import org.hibernate.validator.constraints.NotEmpty;
@AuthNumberMatch(recipient = "loginId", authField = "authNumber")
@PasswordMatch(input = "password", confirm = "password2")
@Data
@PasswordRuleForKbank(password = "password", loginId = "loginId", mobile = "mobileNumber")
@PasswordRuleForKjbank(password = "password", loginId = "loginId", mobile = "mobileNumber")
public class PortalUserRegistrationDTO {
/**
@@ -1,8 +1,7 @@
package com.eactive.apim.portal.apps.user.dto;
import com.eactive.apim.portal.common.validator.PasswordMatch;
import com.eactive.apim.portal.common.validator.PasswordRule;
import com.eactive.apim.portal.common.validator.PasswordRuleForKbank;
import com.eactive.apim.portal.common.validator.PasswordRuleForKjbank;
import com.eactive.apim.portal.common.validator.UniqueId;
import com.eactive.apim.portal.portaluser.entity.UserStatus;
import lombok.Data;
@@ -14,7 +13,7 @@ import java.io.Serializable;
@PasswordMatch(input = "password", confirm = "password2")
@Data
@PasswordRuleForKbank(loginId = "userId", password = "password", mobile = "mobilePhone")
@PasswordRuleForKjbank(loginId = "userId", password = "password", mobile = "mobilePhone")
public class UserRegisterDTO implements Serializable {
@@ -19,8 +19,8 @@ import com.eactive.apim.portal.file.service.FileTypeContext;
import com.eactive.apim.portal.portalorg.entity.PortalOrg;
import com.eactive.apim.portal.portaluser.entity.PortalUser;
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums;
import com.eactive.apim.portal.portaluser.event.UserVerificationEmailEvent;
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.service.MessageHandlerService;
import com.eactive.apim.portal.template.service.MessageRecipient;
import lombok.RequiredArgsConstructor;
@@ -206,7 +206,7 @@ public class OrgRegisterFacadeImpl implements OrgRegisterFacade {
try {
String encToken = encryptionUtil.encrypt(tokenValue);
params.put("token", Base64.encode(encToken.getBytes(StandardCharsets.UTF_8)));
messageHandlerService.publishEvent(UserVerificationEmailEvent.KEY, recipient, params);
messageHandlerService.publishEvent(MessageCode.USER_VERIFICATION_EMAIL, recipient, params);
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
throw new SystemException("암호화 모듈 오류");
}
@@ -15,4 +15,6 @@ public interface UserFacade {
void updateCorporateManager(PortalUserDTO portalUserDTO);
void withdrawUser(String userId);
void activateUserByEmail(String email);
}
@@ -198,4 +198,23 @@ public class UserFacadeImpl implements UserFacade {
throw new IllegalArgumentException("새 비밀번호와 확인 비밀번호가 일치하지 않습니다.");
}
}
@Override
@Transactional
public void activateUserByEmail(String email) {
PortalUser user = portalUserService.findByEmailAddr(email);
if (user == null) {
throw new IllegalArgumentException("사용자를 찾을 수 없습니다.");
}
if (user.getUserStatus() == PortalUserEnums.UserStatus.ACTIVE) {
throw new IllegalArgumentException("이미 활성화된 계정입니다.");
}
user.setUserStatus(PortalUserEnums.UserStatus.ACTIVE);
portalUserService.save(user);
log.info("사용자 이메일 인증 완료 - 활성화: {}", email);
}
}
@@ -10,6 +10,7 @@ import com.eactive.apim.portal.common.util.SecurityUtil;
import com.eactive.apim.portal.common.util.StringMaskingUtil;
import com.eactive.apim.portal.invitation.entity.UserInvitation;
import com.eactive.apim.portal.invitation.entity.UserInvitationEnums.InvitationStatus;
import com.eactive.apim.portal.invitation.event.UserInvitationCancelEvent;
import com.eactive.apim.portal.invitation.event.UserInvitationEvent;
import com.eactive.apim.portal.portaluser.event.UserManagerAssignedEvent;
import com.eactive.apim.portal.invitation.repository.UserInvitationRepository;
@@ -183,7 +184,29 @@ public class UserManFacade {
throw new IllegalStateException("Invitation is not in a cancelable state");
}
// invitationId 로 사용자 이름 조회, 실패시 id 를 이름으로 대체
String name = portalUserRepository.findById(invitation.getInvitationEmail())
.map(PortalUser::getUserName)
.orElse(invitation.getInvitationEmail());
// name 이 이메일 일 경우 @ 앞부분 만 사용 (안하면 암호화 해야함...)
if (name.contains("@")) {
name = name.substring(0, name.indexOf("@"));
}
invitation.setStatus(InvitationStatus.CANCELED);
messageHandlerService.publishEvent(UserInvitationCancelEvent.KEY,
MessageRecipient.builder()
.username(name)
.userId(invitationId)
.build(),
new HashMap<String, Object>() {{
put("corpName", user.getPortalOrg().getOrgName());
put("managerName", user.getUserName());
}}
);
userInvitationRepository.save(invitation);
}
@@ -16,8 +16,8 @@ import com.eactive.apim.portal.invitation.entity.UserInvitation;
import com.eactive.apim.portal.invitation.entity.UserInvitationEnums;
import com.eactive.apim.portal.invitation.repository.UserInvitationRepository;
import com.eactive.apim.portal.portaluser.entity.PortalUser;
import com.eactive.apim.portal.portaluser.event.UserVerificationEmailEvent;
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.service.MessageHandlerService;
import com.eactive.apim.portal.template.service.MessageRecipient;
import lombok.RequiredArgsConstructor;
@@ -146,7 +146,8 @@ public class UserRegisterFacadeImpl implements UserRegisterFacade {
}
agreementsFacade.saveUserAgreements(newUser.getId(), AgreementType.PRIVACY_COLLECT_IND);
sendEmailActivation(newUser);
// 11.13 - 회원 가입단계가 아닌 로그인 단계로 이메일 인증 이동
// sendEmailActivation(newUser);
return new ValidationResponse(true,"회원가입이 완료되었습니다.");
}
@@ -219,7 +220,7 @@ public class UserRegisterFacadeImpl implements UserRegisterFacade {
String tokenValue = String.valueOf(authNumberGenerator.generateAuthNumber());
// String tokenValue = registeredUser.getCreatedDate().format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")) + ":" + registeredUser.getId();
params.put("token", tokenValue);
messageHandlerService.publishEvent(UserVerificationEmailEvent.KEY, recipient, params);
messageHandlerService.publishEvent(MessageCode.USER_VERIFICATION_EMAIL, recipient, params);
// try {
// String encToken = encryptionUtil.encrypt(tokenValue);
@@ -96,9 +96,10 @@ public class PortalUserAuthService implements UserDetailsService {
public void initializePassword(String loginId, String userName, String mobileNumber) {
PortalUser portalUser = portalUserRepository.findByLoginIdAndUserNameAndMobileNumber(loginId, userName, mobileNumber).orElseThrow(() -> new UserNotFoundException("일치하는 사용자 정보를 찾을 수 없습니다."));
PortalUser portalUser = portalUserRepository.findByLoginIdAndUserNameAndMobileNumber(loginId, userName, mobileNumber)
.orElseThrow(() -> new UserNotFoundException("일치하는 사용자 정보를 찾을 수 없습니다."));
String tempPassword = EncryptionUtil.generateNewPassword();
String tempPassword = EncryptionUtil.generateNewPasswordForKjbank(loginId);
portalUser.setPasswordHash(passwordEncoder.encode(tempPassword));
portalUserRepository.save(portalUser);
@@ -49,6 +49,15 @@ public class PortalUserService {
return portalUserRepository.findByLoginId(loginId);
}
public PortalUser findByEmailAddr(String emailAddr) {
return portalUserRepository.findPortalUserByEmailAddr(emailAddr)
.orElseThrow(() -> new IllegalArgumentException("해당 이메일 주소의 사용자를 찾을 수 없습니다."));
}
public PortalUser save(PortalUser user) {
return portalUserRepository.save(user);
}
public boolean existsByLoginId(String loginId) {
return portalUserRepository.existsByLoginId(loginId);
}
@@ -1,6 +1,7 @@
package com.eactive.apim.portal.apps.user.validator;
import com.eactive.apim.portal.common.validator.PasswordRuleForKbankValidator;
import com.eactive.apim.portal.common.validator.PasswordRuleForKjbankValidator;
import org.springframework.stereotype.Component;
@Component
@@ -16,7 +17,7 @@ public class PasswordValidator {
}
public boolean isValidPassword(String password, String loginId, String mobileNumber) {
PasswordRuleForKbankValidator validator = new PasswordRuleForKbankValidator();
PasswordRuleForKjbankValidator validator = new PasswordRuleForKjbankValidator();
return validator.isValid(password, loginId, mobileNumber);
}
@@ -40,7 +40,7 @@ public class PortalAuthenticatedUser extends PortalUser implements UserDetails {
@Override
public boolean isAccountNonLocked() {
return true;
return !"Y".equalsIgnoreCase(this.getAccountLockYn());
}
@Override
@@ -30,7 +30,30 @@ public class EncryptionUtil {
return newpassword.toString();
}
@Value("${encryption.key:kbank_portal_application_1357902}")
/**
* 광주은행용 임시패스워드 생성
* - @ + 이메일 주소 앞 5자리 + 랜덤숫자 3자리 + !
* @return
*/
public static String generateNewPasswordForKjbank(String loginId) {
StringBuilder newpassword = new StringBuilder();
newpassword.append("@");
if (loginId.length() <= 5) {
newpassword.append(loginId);
} else {
newpassword.append(loginId.substring(0, 5));
}
for (int i = 1; i <= 3; i++) {
newpassword.append(RandomUtil.getRandomNum(0, 9));
}
newpassword.append("!");
return newpassword.toString();
}
@Value("${encryption.key:kjbank_portal_application_1357902}")
private String secretKey; // Should be 16, 24, or 32 bytes long for AES-128, AES-192, or AES-256
private static final String ALGORITHM = "AES";
@@ -0,0 +1,24 @@
package com.eactive.apim.portal.common.validator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Constraint(validatedBy = PasswordRuleForKjbankValidator.class)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PasswordRuleForKjbank {
String message() default "비밀 번호 규칙에 부합하지 않습니다.(영문/숫자/특수문자 포함 8~20자, 아이디, 휴대전화, 3자리 이상 연속,반복 문자 불가)";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String password();
String loginId();
String mobile();
}
@@ -0,0 +1,144 @@
package com.eactive.apim.portal.common.validator;
import org.apache.commons.beanutils.PropertyUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Created by Sungpil Hyun
*/
public class PasswordRuleForKjbankValidator implements ConstraintValidator<PasswordRuleForKjbank, Object> {
// 최소 8자, 최대 20자 상수 선언
private static final int MIN = 8;
private static final int MAX = 20;
private String password;
private String loginId;
private String mobileNumber;
// 3자리 연속 문자 정규식
private static final String SAMEPT = "(\\w)\\1\\1";
// 공백 문자 정규식
private static final String BLANKPT = "(\\s)";
@Override
public void initialize(PasswordRuleForKjbank constraintAnnotation) {
this.password = constraintAnnotation.password();
this.loginId = constraintAnnotation.loginId();
this.mobileNumber = constraintAnnotation.mobile();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
String passwordValue = null;
String loginIdValue = null;
String mobileNumberValue = null;
try {
passwordValue = (String) PropertyUtils.getProperty(value, this.password);
loginIdValue = (String) PropertyUtils.getProperty(value, this.loginId);
mobileNumberValue = (String) PropertyUtils.getProperty(value, this.mobileNumber);
} catch (Exception e) {
return false;
}
return isValid(passwordValue, loginIdValue, mobileNumberValue);
}
public boolean isValid(String password, String loginId, String mobileNumber) {
// 영어, 숫자, 특수문자 포함한 MIN to MAX 글자 정규식
String REGEX = "^((?=.*\\d)(?=.*[a-zA-Z])(?=.*[\\W]).{" + MIN + "," + MAX + "})$";
// 정규식 검사객체
Matcher matcher;
// 공백 체크
if (password == null || "".equals(password)) {
return false;
}
// ASCII 문자 비교를 위한 UpperCase
String tmpPw = password.toUpperCase();
// 문자열 길이
int strLen = tmpPw.length();
// 글자 길이 체크
if (strLen > 20 || strLen < 8) {
return false;
}
if (loginId != null && !loginId.isEmpty()) {
String[] loginParts = loginId.split("@");
if (loginParts.length > 0) {
String username = loginParts[0].toUpperCase();
if (tmpPw.contains(username)) {
return false;
}
}
}
// Mobile number validation
if (mobileNumber != null && !mobileNumber.isEmpty()) {
String[] mobileParts = mobileNumber.split("-");
for (String part : mobileParts) {
if (!part.isEmpty() && tmpPw.contains(part)) {
return false;
}
}
}
// 공백 체크
matcher = Pattern.compile(BLANKPT).matcher(tmpPw);
if (matcher.find()) {
return false;
}
// 비밀번호 정규식 체크
matcher = Pattern.compile(REGEX).matcher(tmpPw);
if (!matcher.find()) {
return false;
}
// 동일한 문자 3개 이상 체크
matcher = Pattern.compile(SAMEPT).matcher(tmpPw);
if (matcher.find()) {
return false;
}
// 연속된 문자 / 숫자 3개 이상 체크
// ASCII Char를 담을 배열 선언
int[] tmpArray = new int[strLen];
// Make Array
for (int i = 0; i < strLen; i++) {
tmpArray[i] = tmpPw.charAt(i);
}
// Validation Array
for (int i = 0; i < strLen - 2; i++) {
if (isContinuous(tmpArray[i], tmpArray[i + 2]) && isContinuous(tmpArray[i], tmpArray[i + 1], tmpArray[i + 2])) {
return false;
}
}
// Validation Complete
return true;
}
static boolean isContinuous(int first, int third) {
// 첫 글자 A-Z / 0-9
return (first > 47 && third < 58) || (first > 64 && third < 91);
}
static boolean isContinuous(int first, int second, int third) {
// 배열의 연속된 수 검사
// 3번째 글자 - 2번째 글자 = 1, 3번째 글자 - 1번째 글자 = 2
return Math.abs(third - second) == 1 && Math.abs(third - first) == 2;
}
}
@@ -73,6 +73,7 @@ public class BaseDatasourceConfiguration {
properties.put("hibernate.default_schema", prop.getSchema());
properties.put("javax.persistence.validation.mode", "none");
properties.put("javax.persistence.transactionType", "JTA");
properties.put("hibernate.format_sql", "true");
if (prop instanceof EmsDatasourceProperty) {
persistenceUnit = "portal";
@@ -95,7 +96,7 @@ public class BaseDatasourceConfiguration {
}
// 개발 환경용
if (env.matchesProfiles("dev", "local")) {
if (env.matchesProfiles("local")) {
properties.put("hibernate.hbm2ddl.auto", "validate");
}
@@ -1,7 +1,6 @@
package com.eactive.apim.portal.config;
import com.eactive.eai.data.jpa.BaseRepositoryImpl;
import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -9,20 +8,14 @@ import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jndi.JndiObjectFactoryBean;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import javax.annotation.PostConstruct;
import javax.naming.NamingException;
import javax.sql.DataSource;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
@Configuration
@EntityScan(basePackages = {"com.eactive.apim.gateway"})
@@ -78,28 +71,6 @@ public class GatewayDatasourceConfiguration extends BaseDatasourceConfiguration
return createDataSource(gatewayProp);
}
@Primary
@Bean
public DataSource portalDataSource(EmsDatasourceProperty emsProp) {
// JNDI 이용
if (StringUtils.isNotEmpty(emsProp.getJndiName())) {
try {
return this.jndiLookup(emsProp.getJndiName());
} catch (NamingException e) {
log.warn("JNDI lookup failed.", e);
}
}
log.info("개발환경을 위해 JNDI 대신 직접 DB 접속 시도");
// JNDI 실패시 직접 접속 이용
if (StringUtils.isEmpty(emsProp.getJdbcUrl())) {
throw new IllegalArgumentException("DB 접속을 위한 JNDI 또는 서버 접속 정보가 설정되어 있지 않음");
}
return createDataSource(emsProp);
}
@Bean(name = "gatewayEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean gatewayEntityManagerFactory(
EntityManagerFactoryBuilder builder,
@@ -107,18 +78,4 @@ public class GatewayDatasourceConfiguration extends BaseDatasourceConfiguration
GatewayDataSourceProperty gatewayProp) {
return createEntityManagerFactory(builder, dataSource, gatewayProp);
}
@Primary
@Bean(name = "entityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
EntityManagerFactoryBuilder builder,
@Qualifier("gatewayDataSource") DataSource dataSource,
EmsDatasourceProperty emsProp) {
return createEntityManagerFactory(builder, dataSource, emsProp);
}
@Bean
public AuditorAware<String> auditorProvider() {
return new AuditorAwareImpl();
}
}
@@ -5,11 +5,10 @@ import com.eactive.apim.portal.apps.user.service.PortalUserLogService;
import com.eactive.apim.portal.common.exception.UserNotFoundException;
import com.eactive.apim.portal.portaluser.entity.PortalUser;
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.service.MessageHandlerService;
import com.eactive.apim.portal.template.service.MessageRecipient;
import org.apache.groovy.util.Maps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
@@ -20,6 +19,11 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
/**
* Created by Sungpil Hyun
*/
@@ -30,10 +34,15 @@ public class PortalAuthenticationFailureHandler implements AuthenticationFailure
private static final Logger logger = LoggerFactory.getLogger(PortalAuthenticationFailureHandler.class);
private final PortalUserRepository portalUserRepository;
private final PortalUserLogService userLogService;
private final MessageHandlerService messageHandlerService;
public PortalAuthenticationFailureHandler(PortalUserRepository portalUserRepository, PortalUserLogService userLogService) {
public PortalAuthenticationFailureHandler(PortalUserRepository portalUserRepository,
PortalUserLogService userLogService,
MessageHandlerService messageHandlerService) {
this.portalUserRepository = portalUserRepository;
this.userLogService = userLogService;
this.messageHandlerService = messageHandlerService;
}
@Override
@@ -53,8 +62,17 @@ public class PortalAuthenticationFailureHandler implements AuthenticationFailure
user.setLoginFailureCount(user.getLoginFailureCount() + 1);
if (user.getLoginFailureCount() >= 5) {
user.setAccountLockYn("Y");
// 계정 잠금 알림
messageHandlerService.publishEvent(
MessageCode.USER_ACCOUNT_LOCKED,
MessageRecipient.of(user),
Maps.of("reason", "5회 이상 로그인 실패로 인한 계정 잠금")) ;;
}
portalUserRepository.save(user);
} catch (UserNotFoundException e) {
logger.error("{} login try {}", username, e.getMessage());
}
@@ -5,8 +5,12 @@ import com.eactive.apim.portal.apps.user.service.PortalUserAuthService;
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
import com.eactive.apim.portal.portalorg.entity.PortalOrgEnums;
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.service.MessageHandlerService;
import com.eactive.apim.portal.template.service.MessageRecipient;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.groovy.util.Maps;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
@@ -25,6 +29,7 @@ public class PortalAuthenticationManager implements AuthenticationManager {
private final PortalUserAuthService portalUserAuthService;
private final PasswordEncoder passwordEncoder;
private final MessageHandlerService messageHandlerService;
private String decodePassword(String encodedPassword) {
try {
@@ -46,9 +51,22 @@ public class PortalAuthenticationManager implements AuthenticationManager {
PortalAuthenticatedUser user = (PortalAuthenticatedUser) portalUserAuthService.loadUserByUsername(username);
if (!user.isAccountNonLocked()) {
if (user.getLoginFailureCount() >= 5) {
throw new LockedException("계정이 잠겼습니다. 비밀번호 초기화 또는 관리자에게 문의하세요.");
}
throw new LockedException("로그인 할 수 없습니다. 비밀번호 초기화 또는 관리자에게 문의하세요.");
}
// 비밀번호 검증
if (!passwordEncoder.matches(decodedPassword, user.getPassword())) {
throw new BadCredentialsException("아이디 또는 비밀번호가 일치하지 않습니다.");
// 암호화가 되어 있지 않을 수도 있어 그대로 비교 검증
if (!user.getPassword().equals(decodedPassword)) {
throw new BadCredentialsException("아이디 또는 비밀번호가 일치하지 않습니다.");
}
}
// DORMANT 상태 체크
@@ -64,9 +82,10 @@ public class PortalAuthenticationManager implements AuthenticationManager {
throw new DisabledException("사용자 승인 대기중입니다. ");
}
if (user.getUserStatus().equals(PortalUserEnums.UserStatus.READY)) {
throw new DisabledException("이메일 인증이 완료되지 않았습니다. 인증 메일을 확인해주세요.");
}
// 25.11.13 - 이메일 인증 로그인은 허용
// if (user.getUserStatus().equals(PortalUserEnums.UserStatus.READY)) {
// throw new DisabledException("이메일 인증이 완료되지 않았습니다. 인증 메일을 확인해주세요.");
// }
if (user.getUserStatus().equals(PortalUserEnums.UserStatus.ADMINBLOCK)) {
throw new DisabledException("법인 관리자에 의해 비활성화된 계정입니다. 법인 관리자에게 문의하시기 바랍니다.");
@@ -79,9 +98,7 @@ public class PortalAuthenticationManager implements AuthenticationManager {
}
}
if (!user.isAccountNonLocked()) {
throw new LockedException("로그인할 수 없습니다. 관리자에게 문의하세요.");
}
UsernamePasswordAuthenticationToken authorityToken =
new UsernamePasswordAuthenticationToken(user, decodedPassword, user.getAuthorities());
@@ -55,7 +55,11 @@ public class PortalAuthenticationSuccessHandler implements AuthenticationSuccess
HttpSession session = request.getSession();
// 세션에 상태 저장
if (isDormantAccount(user)) {
if (isEmailVerificationRequired(user)) {
session.setAttribute("success", "이메일 인증이 완료되지 않았습니다. 이메일을 확인하여 인증을 완료해주세요.");
session.setAttribute("emailVerificationRequired", true);
session.setAttribute("redirectUrl", contextPath + "/mypage/verification-email");
} else if (isDormantAccount(user)) {
session.setAttribute("success", "90일 이상 미접속하여 계정이 잠금 처리되었습니다. 본인인증 후 이용해주세요.");
session.setAttribute("dormantAccount", true);
session.setAttribute("dormantLoginId", username);
@@ -114,4 +118,8 @@ public class PortalAuthenticationSuccessHandler implements AuthenticationSuccess
return PortalUserEnums.UserStatus.DORMANT.equals(user.getUserStatus());
}
private boolean isEmailVerificationRequired(PortalUser user) {
return PortalUserEnums.UserStatus.READY.equals(user.getUserStatus());
}
}
@@ -1,5 +1,6 @@
package com.eactive.apim.portal.config;
import com.eactive.apim.portal.spring.DatabaseSessionVerifier;
import com.eactive.eai.data.jpa.BaseRepositoryImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@@ -57,23 +58,39 @@ public class PortalDatasourceConfiguration extends BaseDatasourceConfiguration {
@Primary
@Bean
public DataSource portalDataSource(EmsDatasourceProperty emsProp) {
DataSource dataSource = null;
log.debug("Portal datasource initialize.");
// JNDI 이용
if (StringUtils.isNotEmpty(emsProp.getJndiName())) {
try {
return this.jndiLookup(emsProp.getJndiName());
log.debug("Try get JNDI datasource : {}", emsProp.getJndiName());
dataSource = this.jndiLookup(emsProp.getJndiName());
} catch (NamingException e) {
log.warn("JNDI lookup failed.", e);
}
}
log.info("개발환경을 위해 JNDI 대신 직접 DB 접속 시도");
if (dataSource == null) {
log.info("개발환경을 위해 JNDI 대신 직접 DB 접속 시도");
// JNDI 실패시 직접 접속 이용
if (StringUtils.isEmpty(emsProp.getJdbcUrl())) {
throw new IllegalArgumentException("DB 접속을 위한 JNDI 또는 서버 접속 정보가 설정되어 있지 않음");
// JNDI 실패시 직접 접속 이용
if (StringUtils.isEmpty(emsProp.getJdbcUrl())) {
throw new IllegalArgumentException("DB 접속을 위한 JNDI 또는 서버 접속 정보가 설정되어 있지 않음");
}
dataSource = createDataSource(emsProp);
}
return createDataSource(emsProp);
if (dataSource != null && env.matchesProfiles("db_check")) {
log.debug("Activated 'db_check' profile.");
DatabaseSessionVerifier verifier = new DatabaseSessionVerifier(dataSource);
verifier.afterSingletonsInstantiated();
}
return dataSource;
}
@Primary
@@ -0,0 +1,100 @@
package com.eactive.apim.portal.spring;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.SmartInitializingSingleton;
import javax.sql.DataSource;
import java.sql.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
//@Configuration
//@Profile("db_check")
@Slf4j
public class DatabaseSessionVerifier implements SmartInitializingSingleton {
private final DataSource emsDataSource;
public DatabaseSessionVerifier(DataSource emsDataSource) {
this.emsDataSource = emsDataSource;
}
@Override
public void afterSingletonsInstantiated() {
log.info("DB 검증 작업 시작 - {}", now());
log.info("---------------------------------------------------");
log.info("1. 커넥션 확인");
try (Connection conn = emsDataSource.getConnection()) {
try (PreparedStatement statement = conn.prepareStatement("select 1 from dual")) {
ResultSet rs = statement.executeQuery();
if (rs.next()) {
log.info("DB 커넥션 확인됨");
}
}
log.info("\n2. Table 권한 확인 - {}", now());
Map<String, Set<String>> grantTables = this.printTablePrivileges(conn);
log.info("\n3. PTL_PROPERTY 테이블 확인 - {}", now());
if (!grantTables.containsKey("PTL_PROPERTY")) {
log.info("PTL_PROPERTY 테이블 확인 안됨. ROLE 재로딩 시도");
try (Statement stmt = conn.createStatement()) {
stmt.execute("SET ROLE_RL_EMS_ALL");
}
this.printTablePrivileges(conn);
throw new InterruptedException("DB 검증 실패로 강제 종료");
} else {
log.info("PTL_PROPERTY 테이블 확인 됨.");
}
log.info("검증 작업 종료 - {}", now());
} catch (Exception e) {
log.error("DB 세션 검증 실패", e);
throw new RuntimeException(e);
}
throw new RuntimeException("db_check profile end.");
}
private LinkedHashMap<String, Set<String>> printTablePrivileges(Connection conn) throws SQLException {
LinkedHashMap<String, Set<String>> tables = new LinkedHashMap<>();
try (PreparedStatement stmt = conn.prepareStatement("SELECT * FROM ROLE_TAB_PRIVS WHERE role = ? ORDER BY table_name ASC")) {
stmt.setString(1, "RL_EMS_ALL");
ResultSet rs = stmt.executeQuery();
while(rs.next()) {
Set<String> privileges;
if (tables.containsKey(rs.getString("table_name"))) {
privileges = tables.get(rs.getString("table_name"));
} else {
privileges = new LinkedHashSet<>();
tables.put(rs.getString("table_name").toUpperCase(), privileges);
}
privileges.add(rs.getString("privilege"));
}
log.info("| Table | Privileges |");
log.info("|---|---|");
tables.forEach((key, value) -> log.info(String.format("| %-20s | %-36s |", key, value)));
}
return tables;
}
private String now() {
return LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
}
@@ -0,0 +1,28 @@
package com.eactive.apim.portal.tools;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.sql.SQLException;
@Component
@Aspect
@Slf4j
public class JpaErrorLoggingAspect {
@PersistenceContext
private EntityManager em;
@AfterThrowing(pointcut = "@annotation(org.springframework.transaction.annotation.Transactional)", throwing = "ex")
public void onError(SQLException ex) {
try {
String user = (String) em.createNativeQuery("select user from dual").getSingleResult();
log.error("DB USER = {}, SQL ERROR = {}", user, ex.getMessage() );
} catch (Exception e) {
log.error("Failed to fetch DB User", e);
}
}
}
+17
View File
@@ -18,6 +18,23 @@ spring:
thymeleaf:
cache: false
ems:
datasource:
jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/DAPM
# jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/PAPM
driver-class-name: oracle.jdbc.OracleDriver
username: EMSAPP
password: EMSAPP123!
gateway:
datasource:
jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/DAPM
# jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/PAPM
driver-class-name: oracle.jdbc.OracleDriver
username: AGWAPP
password: AGWAPP123!
portal:
auth-virtual-code: 654321
+4 -4
View File
@@ -19,8 +19,8 @@ spring:
ems:
datasource:
# jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/DAPM
jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/PAPM
jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/DAPM
# jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/PAPM
driver-class-name: oracle.jdbc.OracleDriver
username: EMSAPP
password: EMSAPP123!
@@ -28,8 +28,8 @@ ems:
gateway:
datasource:
# jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/DAPM
jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/PAPM
jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/DAPM
# jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/PAPM
driver-class-name: oracle.jdbc.OracleDriver
username: AGWAPP
password: AGWAPP123!
@@ -0,0 +1,51 @@
server.port: '30200'
spring:
jta:
enabled: true
jpa:
properties:
hibernate:
show_sql: false
format_sql: true
use_sql_comments: true
devtools:
restart:
enabled: false
livereload:
enabled: true
thymeleaf:
cache: false
ems:
datasource:
# jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/DAPM
jdbc-url: jdbc:oracle:thin:@//10.14.8.30:11521/ELINKDB
driver-class-name: oracle.jdbc.OracleDriver
username: EMSAPP
password: EMSAPP123!
gateway:
datasource:
# jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/DAPM
jdbc-url: jdbc:oracle:thin:@//10.14.8.30:11521/ELINKDB
driver-class-name: oracle.jdbc.OracleDriver
username: AGWAPP
password: AGWAPP123!
portal:
logging:
log-path: C:\eactive\logs
auth-virtual-code: 654321
proxy:
targets:
stg: http://localhost:10000/monitoring
prod: http://localhost:10000/monitoring
# stg: http://inter-ndapiap01.k-bank.com:7090/monitoring
# prod: http://inter-ndapiap01.k-bank.com:7090/monitoring
timeout:
connect: 5000
read: 5000
+19
View File
@@ -18,6 +18,25 @@ spring:
portal:
auth-virtual-code: 654321
ems:
datasource:
jndi-name: jdbc/dsOBP_EMS
schema: EMSADM
hibernate-dialect: org.hibernate.dialect.Oracle12cDialect
hibernate-physical-naming-strategy: com.eactive.apim.portal.common.entity.CustomPhysicalNamingStrategy
entity-package: com.eactive.apim.portal
gateway:
datasource:
jndi-name: jdbc/dsOBP_AGW
schema: AGWADM
hibernate-dialect: org.hibernate.dialect.Oracle12cDialect
hibernate-physical-naming-strategy: com.eactive.apim.portal.common.entity.CustomPhysicalNamingStrategy
entity-package: com.eactive.eai.data.entity.onl
#proxy:
# targets:
# stg: http://inter-dapiwas01.k-bank.com:7090/monitoring
+3
View File
@@ -14,6 +14,7 @@ server:
whitelabel:
enabled: false
path: /error
forward-headers-strategy: native
spring:
@@ -74,6 +75,8 @@ portal:
log-path: /Log/App/eapim/
auth-ttl: 300
auth:
resend_limit_seconds: 30
user-approval: true
password-expiration-days: 90
+68 -7
View File
@@ -1,21 +1,82 @@
<configuration scan="true" scanPeriod="5 seconds">
<property name="LOG_PATH" value="/Log/App/eapim//${inst.Name:-devSvrUD}"/>
<appendar name="FILE_DEBUG" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
</encoder>
</appendar>
<property name="PATTERN" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n" />
<property name="PATTERN_DATE" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%-15thread] %-5level %logger - %msg%n" />
<logger name="org.hibernate.engine.jdbc.spi.SqlExceptionHelper" level="DEBUG" additivity="false">
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder><pattern>${PATTERN_DATE}</pattern></encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/debug.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/backup/debug.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder><pattern>${PATTERN}</pattern></encoder>
</appender>
<appender name="FILE_DEBUG" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE_HIBERNATE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/debug-hibernate.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/backup/debug-hibernate.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
<pattern>${PATTERN}</pattern>
</encoder>
</appender>
<appender name="FILE_REPORT" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/debug-report.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/backup/debug-report.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="FILE"/>
</root>
<logger name="com.eactive.eapim" level="DEBUG" />
<logger name="weblogic" level="INFO" />
<logger name="org.springframework" level="info" />
<logger name="_org.springframework" level="info" />
<!-- Hibernate SQL Logging -->
<logger name="org.hibernate.SQL" level="DEBUG" additivity="false">
<appender-ref ref="FILE_HIBERNATE" />
</logger>
<logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE" additivity="false">
<appender-ref ref="FILE_HIBERNATE" />
</logger>
<logger name="org.hibernate.engine.transaction" level="DEBUG" additivity="false">
<appender-ref ref="FILE_HIBERNATE" />
</logger>
<logger name="org.hibernate.engine.jdbc.spi.SqlExceptionHelper" level="DEBUG" additivity="false">
<appender-ref ref="FILE_HIBERNATE" />
</logger>
<logger name="org.springframework.transaction.interceptor" level="DEBUG" additivity="false">
<appender-ref ref="FILE_HIBERNATE" />
</logger>
<logger name="org.springframework.orm.jpa" level="DEBUG" additivity="false">
<appender-ref ref="FILE_HIBERNATE" />
</logger>
<logger name="com.atomikos" level="DEBUG" additivity="false">
<appender-ref ref="FILE_HIBERNATE" />
</logger>
</configuration>
+1
View File
@@ -40,6 +40,7 @@
<logger name="com.eactive.apim" level="DEBUG" />
<logger name="org.thymeleaf.templateparser" level="DEBUG" />
<!-- Hibernate SQL Logging -->
+82
View File
@@ -0,0 +1,82 @@
<configuration scan="true" scanPeriod="5 seconds">
<property name="LOG_PATH" value="/eactive/logs/${inst.Name:-devSvr_undefined}"/>
<property name="PATTERN" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n" />
<property name="PATTERN_DATE" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n" />
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder><pattern>${PATTERN_DATE}</pattern></encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/root.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/backup/root.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder><pattern>${PATTERN}</pattern></encoder>
</appender>
<appender name="FILE_HIBERNATE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/hibernate.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/backup/hibernate.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${PATTERN}</pattern>
</encoder>
</appender>
<appender name="FILE_REPORT" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/report.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/backup/report.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="FILE"/>
<appender-ref ref="CONSOLE"/>
</root>
<logger name="com.eactive.eapim" level="DEBUG" />
<!-- hibernate 로그 -->
<logger name="org.hibernate.SQL" level="DEBUG" additivity="false">
<appender-ref ref="FILE_HIBERNATE" />
</logger>
<logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE" additivity="false">
<appender-ref ref="FILE_HIBERNATE" />
</logger>
<logger name="org.hibernate.engine.transaction.internal.TransactionImpl" level="DEBUG" additivity="false">
<appender-ref ref="FILE_HIBERNATE" />
</logger>
<logger name="org.hibernate.Transaction" level="DEBUG" additivity="false">
<appender-ref ref="FILE_HIBERNATE" />
</logger>
<logger name="org.hibernate.engine.jdbc.spi.SqlExceptionHelper" level="DEBUG" additivity="false">
<appender-ref ref="FILE_HIBERNATE" />
</logger>
<logger name="org.springframework.transaction" level="TRACE" additivity="false">
<appender-ref ref="FILE_HIBERNATE" />
</logger>
<logger name="org.springframework.transaction.interceptor" level="TRACE" additivity="false">
<appender-ref ref="FILE_HIBERNATE" />
</logger>
<logger name="org.springframework.orm.jpa.JpaTransactionManager" level="DEBUG" additivity="false">
<appender-ref ref="FILE_HIBERNATE" />
</logger>
<logger name="com.atomikos" level="DEBUG" additivity="false">
<appender-ref ref="FILE_HIBERNATE" />
</logger>
<logger name="com.eactive.apim.portal.spring.DatabaseSessionVerifier" level="debug">
<appender-ref ref="FILE_REPORT" />
</logger>
</configuration>
+1 -1
View File
@@ -102,7 +102,7 @@
</logger>
<logger name="org.hibernate.engine.jdbc.spi.SqlExceptionHelper" additivity="false">
<appender-ref ref="HIBERNATE_APPENDER" />
></logger>
</logger>
<logger name="com.eactive.eapim" level="DEBUG"/>
<logger name="kjb.safedb" level="DEBUG" additivity="false">
+3 -1
View File
@@ -163,7 +163,9 @@ document.addEventListener('DOMContentLoaded', function() {
const sub = document.getElementById('sub');
// 초기에 서브 메뉴 숨기기
sub.style.display = 'none';
if (sub == null) {
sub.style.display = 'none';
}
// 마우스가 메인 네비게이션에 진입했을 때
mainNav.addEventListener('mouseenter', function() {
@@ -1,372 +1,476 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_index_layout}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_index_layout}">
<body>
<th:block layout:fragment="contentFragment">
<section class="hero-section">
<div class="hero-patterns">
<div class="pattern-circle pattern-1"></div>
<div class="pattern-circle pattern-2"></div>
<div class="pattern-dots"></div>
</div>
<div class="container">
<div class="hero-content">
<div class="hero-badge">
<span class="badge-icon">🚀</span>
<span>Wa bank 생태계에 오신 것을 환영합니다!</span>
<section layout:fragment="contentFragment" class="content main">
<script th:src="@{/js/htmx.min.js}"></script>
<!-- main slick -->
<div class="slider-container">
<div class="slider m-only">
<div class="slider_box">
<img th:src="@{/img/img_mainslick_m.png}" alt="슬라이드 1">
<div class="slick_txt">
<p>디지털 금융 리더 <span>케이뱅크</span><br/>새로운 비즈니스 기회를<br/>제공합니다.</p>
</div>
<h1 class="main-headline">
<span class="headline-wa">Wa한</span> 금융 혁신,<br>
여러분의 아이디어로 시작됩니다
</h1>
<h2 class="sub-headline">
광주은행 오픈 API로 새로운 금융 서비스를 만들어보세요.<br>
안전하고 빠르게, 그리고 쉽게 연동할 수 있습니다.
</h2>
<div class="hero-buttons">
<a href="/apis" class="btn btn-primary">
<i class="fas fa-code"></i> API 둘러보기
</a>
<a href="#get-started" class="btn btn-secondary">
<i class="fas fa-play"></i> 5분 만에 시작하기
</a>
</div>
<div class="slider_box">
<img th:src="@{/img/img_mainslick2_m.png}" alt="슬라이드 2">
<div class="slick_txt">
<p>차별화된 <span>API와<br> 편리한 테스트 환경</span><br> 지원합니다</p>
</div>
</div>
<div class="slider_box">
<img th:src="@{/img/img_mainslick3_m.png}" alt="슬라이드 3">
<div class="slick_txt">
<p><span>디지털 금융 혁신</span><br> 케이뱅크가<br> 함께합니다</p>
</div>
</div>
</div>
</section>
<!-- API 검색 섹션 -->
<section class="api-search-section">
<div class="container">
<div class="search-container">
<form th:action="@{/apis}" method="get" class="search-form">
<div class="search-wrapper">
<input type="text" class="search-input" placeholder="원하는 API를 검색해보세요" id="apiSearchInput" name="keyword">
<button class="search-button" type="submit">
<i class="fas fa-search"></i>
<span>검색하기</span>
</button>
</div>
</form>
<div class="hashtag-suggestions" th:if="${hashtags != null and not #lists.isEmpty(hashtags)}">
<span class="hashtag-label">추천 검색어:</span>
<a href="#" class="hashtag" th:each="tag : ${hashtags}"
th:data-search="${tag}"
th:text="${'#' + tag}">
</a>
<div class="slider pc-only">
<div class="slider_box">
<img th:src="@{/img/img_mainslick_pc.png}" alt="슬라이드 1">
<div class="slick_txt">
<p><span>케이뱅크</span>가 새로운 비즈니스<br> 기회를 제공합니다</p>
</div>
<!-- Fallback hashtags if no hashtags from controller -->
<div class="hashtag-suggestions" th:unless="${hashtags != null and not #lists.isEmpty(hashtags)}">
<span class="hashtag-label">추천 검색어:</span>
<a href="#" class="hashtag" data-search="계좌조회">#계좌조회</a>
<a href="#" class="hashtag" data-search="송금API">#송금API</a>
<a href="#" class="hashtag" data-search="실시간결제">#실시간결제</a>
<a href="#" class="hashtag" data-search="오픈뱅킹">#오픈뱅킹</a>
</div>
<div class="slider_box">
<img th:src="@{/img/img_mainslick2_pc.png}" alt="슬라이드 2">
<div class="slick_txt">
<p>차별화된 <span>API</span><br/>
<span>편리한 테스트 환경</span><br/>지원합니다</p>
</div>
</div>
<div class="slider_box">
<img th:src="@{/img/img_mainslick3_pc.png}" alt="슬라이드 3">
<div class="slick_txt">
<p><span>디지털 금융 혁신</span><br>케이뱅크가<br>함께합니다</p>
</div>
</div>
</div>
</section>
<button class="slick-prev" aria-label="Previous" type="button">이전</button>
<button class="slick-next" aria-label="Next" type="button">다음</button>
<ul class="slick-dots">
<li>
<button type="button">1</button>
</li>
<li>
<button type="button">2</button>
</li>
<li>
<button type="button">3</button>
</li>
</ul>
</div>
<!-- // main slick -->
<section class="api-showcase">
<div class="container">
<div class="section-header">
<h2 class="section-title">
<span class="title-highlight">인기 API</span>로 바로 시작하세요
</h2>
<p class="section-subtitle">가장 많이 사용되는 API를 확인하고 바로 테스트해보세요</p>
<div class="main-container">
<div class="search_type">
<form th:action="@{/apis}" id="searchForm" method="get">
<div class="search-container">
<input type="text" name="keyword" class="search-box" id="search-box" placeholder="어떤 API를 찾고 계신가요?">
<button class="search-button" type="submit"></button>
</div>
</form>
</div>
<div class="hashtag">
<ul>
<li th:each="tag: ${hashtags}"><a href="#" th:text="${tag}">#인증</a></li>
</ul>
</div>
<div class="main_keyword">
<div class="api_tit">
<h2>가장 많이 찾는 API</h2>
<p class="total_view">
<a href="/apis">전체보기</a>
</p>
</div>
<div class="api-grid">
<!-- Dynamic API Cards from Controller (max 4) -->
<div th:each="api, iterStat : ${apis}"
th:if="${iterStat.index < 4}"
class="api-card"
th:classappend="${iterStat.index == 0} ? 'api-card-popular' : (${iterStat.index == 3} ? 'api-card-new' : '')">
<!-- Show badge for first (인기) and last (NEW) items -->
<div th:if="${iterStat.index == 0}" class="api-badge">인기</div>
<div th:if="${iterStat.index == 3}" class="api-badge new">NEW</div>
<!-- API Icon - using different icons based on index or API type -->
<div class="api-icon">
<!-- Icon selection based on API name or service type -->
<i th:if="${api.apiName != null}"
th:class="${#strings.containsIgnoreCase(api.apiName, '계좌') or #strings.containsIgnoreCase(api.apiName, '조회')} ? 'fas fa-wallet' :
(${#strings.containsIgnoreCase(api.apiName, '송금') or #strings.containsIgnoreCase(api.apiName, '이체')} ? 'fas fa-exchange-alt' :
(${#strings.containsIgnoreCase(api.apiName, '카드') or #strings.containsIgnoreCase(api.apiName, '결제')} ? 'fas fa-credit-card' :
(${#strings.containsIgnoreCase(api.apiName, 'AI') or #strings.containsIgnoreCase(api.apiName, '분석')} ? 'fas fa-chart-line' :
(${iterStat.index == 0} ? 'fas fa-wallet' :
(${iterStat.index == 1} ? 'fas fa-exchange-alt' :
(${iterStat.index == 2} ? 'fas fa-credit-card' : 'fas fa-chart-line'))))))"></i>
</div>
<!-- API Name -->
<h3 class="api-name" th:text="${api.apiName}">API 이름</h3>
<!-- API Description -->
<p class="api-description" th:text="${api.apiSimpleDescription != null ? api.apiSimpleDescription : api.description}">API 설명</p>
<!-- API Features/Method Info -->
<div class="api-features">
<span class="feature-tag" th:if="${api.apiMethod != null}" th:text="${api.apiMethod}">GET</span>
<span class="feature-tag" th:if="${api.service != null}" th:text="${api.service}">서비스</span>
</div>
<!-- Test Link -->
<a th:href="@{/apis/detail(id=${api.apiId})}" class="api-link">
테스트하기 <i class="fas fa-arrow-right"></i>
</a>
<div class="api_keyword">
<ul class="keyword_list tab_list">
<li>
<a href="#"
hx-get="/api_fragments"
hx-target="#api_spec_list"
hx-trigger="click"
onclick="setActive(this)">전체<span class="number" th:text="${totalApiCount}"></span></a>
</li>
<li th:each="item : ${services}">
<a href="#"
th:hx-get="@{/api_fragments(id=${item.id})}"
hx-target="#api_spec_list"
hx-trigger="click"
onclick="setActive(this)">
[[${item.groupName}]]<span class="number" th:text="${item.apiGroupApiList.size()}"></span>
</a>
</li>
</ul>
<div class="more_type m-only">
<button class="showmore" type="button">
<img th:src="@{/img/icon/icon_more_btn.png}" alt="더보기 버튼">
</button>
</div>
<div class="more_type pc-only">
<button class="showmore" type="button">
<img th:src="@{/img/icon/icon_more_btn2.png}" alt="더보기 버튼">
</button>
</div>
</div>
<!-- Fallback: Show static cards if no APIs from controller -->
<div th:if="${apis == null or #lists.isEmpty(apis)}" class="api-card api-card-popular">
<div class="api-badge">인기</div>
<div class="api-icon">
<i class="fas fa-wallet"></i>
<div class="api_info_container box_list" id="api_spec_list">
<div class="api_info_box" th:each="api : ${apis}" th:data-href="@{/apis/detail(id=${api.apiId})}">
<div class="api_list" th:data-href="@{/apis/detail(id=${api.apiId})}">
<p class="txt1" th:text="${api.apiName}"></p>
<p class="txt2" th:text="${api.apiSimpleDescription}"></p>
</div>
<h3 class="api-name">계좌 조회 API</h3>
<p class="api-description">실시간 잔액 조회와 거래내역을 안전하게 확인</p>
<div class="api-features">
<span class="feature-tag">실시간</span>
<span class="feature-tag">OAuth 2.0</span>
<div class="api_img" style="height: 48px;">
<a th:href="@{/apis/detail(id=${api.apiId})}" class="btn btn-primary">
<img th:if="${#strings.isEmpty(api.mainIcon) == false}" th:src="${api.mainIcon}" alt="">
</a>
</div>
<a href="#" class="api-link">
테스트하기 <i class="fas fa-arrow-right"></i>
</a>
</div>
<div th:if="${apis == null or #lists.isEmpty(apis)}" class="api-card">
<div class="api-icon">
<i class="fas fa-exchange-alt"></i>
</div>
<h3 class="api-name">송금 API</h3>
<p class="api-description">간편하고 안전한 송금 서비스 구현</p>
<div class="api-features">
<span class="feature-tag">간편송금</span>
<span class="feature-tag">실명확인</span>
</div>
<a href="#" class="api-link">
테스트하기 <i class="fas fa-arrow-right"></i>
</a>
</div>
<div th:if="${apis == null or #lists.isEmpty(apis)}" class="api-card">
<div class="api-icon">
<i class="fas fa-credit-card"></i>
</div>
<h3 class="api-name">카드 결제 API</h3>
<p class="api-description">온/오프라인 결제 솔루션 통합</p>
<div class="api-features">
<span class="feature-tag">PG연동</span>
<span class="feature-tag">간편결제</span>
</div>
<a href="#" class="api-link">
테스트하기 <i class="fas fa-arrow-right"></i>
</a>
</div>
<div th:if="${apis == null or #lists.isEmpty(apis)}" class="api-card api-card-new">
<div class="api-badge new">NEW</div>
<div class="api-icon">
<i class="fas fa-chart-line"></i>
</div>
<h3 class="api-name">AI 신용평가 API</h3>
<p class="api-description">머신러닝 기반 실시간 신용 분석</p>
<div class="api-features">
<span class="feature-tag">AI/ML</span>
<span class="feature-tag">실시간</span>
</div>
<a href="#" class="api-link">
테스트하기 <i class="fas fa-arrow-right"></i>
</a>
</div>
</div>
</div>
</section>
<!-- 정보 섹션 -->
<section class="info-section">
<div class="container">
<div class="info-wrapper">
<div class="image-container">
<div class="image-placeholder">
<i class="fas fa-chart-line"></i>
<span>금융 혁신</span>
</div>
</div>
<div class="main_con">
<p class="con_tit m-only"><img th:src="@{/img/img_con_tit.png}" alt="Kbank API PORTAL"></p>
<p class="con_tit pc-only"><img th:src="@{/img/img_con_tit2.png}" alt="Kbank API PORTAL"></p>
<p class="con_img m-only"><img th:src="@{/img/img_mask_group.png}" alt="Kbank API PORTAL"></p>
<p class="con_img pc-only"><img th:src="@{/img/img_mask_group2.png}" alt="Kbank API PORTAL"></p>
<p class="con_txt m-only">
<span>Kbank API Portal</span>은 기업이 금융서비스를<br>
편리하게 개발할 수 있도록 차별화된<br>
API와 테스트환경을 제공합니다.<br>
</p>
<p class="con_txt pc-only">
<span>Kbank API Portal</span>은 기업이 금융서비스를 편리하게 개발할 수 있도록 차별화된 API와 테스트환경을 제공합니다.
</p>
<div class="main_btn">
<a th:href="@{/intro}" class="common_btn_type_5"><span>처음 만나는 케이뱅크 API</span></a>
<a th:href="@{/partnership}" class="common_btn_type_5 m_btn"><span>사업 제휴 문의</span></a>
</div>
</div>
</div>
<div class="info-content">
<p class="highlight-text">
광주은행 <span class="text-accent">API Portal</span>은 기업이 금융서비스를 편리하게 개발할 수 있도록<br>
차별화된 API와 테스트환경을 제공합니다.
<div class="tiding_box">
<div class="api_type">
<div class="api_tiding">
<div class="api_tit">
<h2>새로운 케이뱅크 API 소식을<br> 만나보세요</h2>
<p class="total_view">
<a href="/portalnotice">전체보기</a>
</p>
<div class="action-buttons">
<a href="#" class="action-btn">
<span class="btn-number">1</span>
<span class="btn-text">처음 만나는 광주뱅크 API</span>
<i class="fas fa-arrow-right"></i>
</div>
<div class="api_tiding_list">
<ul>
<li th:each="notice : ${portalNotices}">
<a th:href="@{/portalnotice/detail(id=${notice.id})}">
<p th:text="${notice.noticeSubject}"></p>
</a>
<span th:text="${#temporals.format(notice.createdDate, 'yyyy.MM.dd')}"></span>
</li>
</ul>
</div>
</div>
<div class="api_info">
<ul>
<li class="img1">
<a th:href="@{/faq_list}">
<p>FAQ</p>
<span>자주 묻는 질문과 답변을<br> 확인해보세요</span>
<img th:src="@{/img/icon/icon_petals01.png}" alt="faq 이미지">
</a>
<a href="#" class="action-btn">
<span class="btn-number">2</span>
<span class="btn-text">사업 제휴 문의</span>
<i class="fas fa-arrow-right"></i>
</li>
<li class="img2">
<a th:href="@{/inquiry}">
<p>Q&amp;A</p>
<span>문의사항에<br> 친절하게 답변 드리겠습니다</span>
<img th:src="@{/img/icon/icon_petals02.png}" alt="Q&A 이미지">
</a>
</div>
</div>
</li>
<li class="img3">
<a th:href="@{/partnership}">
<p>사업제휴</p>
<span>온라인 비즈니스 혁신을 위한<br> 사업 제안을 환영합니다</span>
<img th:src="@{/img/icon/icon_petals03.png}" alt="사업제휴 이미지">
</a>
</li>
</ul>
</div>
</div>
</section>
</div>
<!-- 지원 센터 섹션 -->
<section class="support-center">
<div class="container">
<div class="section-header">
<h2 class="section-title">
도움이 <span class="title-highlight">필요하신가요?</span>
</h2>
<p class="section-subtitle">다양한 방법으로 필요한 정보를 찾아보세요</p>
</div>
<div class="support-grid">
<a th:href="@{/portalnotice}" class="support-card">
<div class="card-icon">
<i class="fas fa-bullhorn"></i>
</div>
<h3>공지사항</h3>
<p>최신 업데이트와 중요한 소식을 확인하세요</p>
<div class="card-arrow">
<i class="fas fa-arrow-right"></i>
</div>
</a>
<a th:href="@{/faq_list}" class="support-card">
<div class="card-icon">
<i class="fas fa-question-circle"></i>
</div>
<h3>자주 묻는 질문</h3>
<p>자주 묻는 질문과 답변을 빠르게 찾아보세요</p>
<div class="card-arrow">
<i class="fas fa-arrow-right"></i>
</div>
</a>
<a th:href="@{/inquiry}" class="support-card">
<div class="card-icon">
<i class="fas fa-comments"></i>
</div>
<h3>문의하기</h3>
<p>추가 도움이 필요하시면 직접 문의해주세요</p>
<div class="card-arrow">
<i class="fas fa-arrow-right"></i>
</div>
</a>
<div class="api_count">
<div class="api_total_box">
<p class="txt m-only">우리곁의 수많은 서비스들이<br> <em>케이뱅크API</em><br> 함께하고 있습니다</p>
<p class="txt pc-only">우리곁의 수많은 서비스들이<br> <em>케이뱅크API와 함께</em>하고 있습니다</p>
<div class="api_total">
<ul>
<li>
<img th:src="@{/img/icon/icon_api01.png}" alt="서비스 이용 수">
<div class="api_num">
<p th:text="${serviceCount}"></p>
<span>서비스 이용</span>
</div>
</li>
<li>
<img th:src="@{/img/icon/icon_api02.png}" alt="API 이용건수">
<div class="api_num">
<p th:text="${selectedApiCount}"></p>
<span>API 이용</span>
</div>
</li>
<li>
<img th:src="@{/img/icon/icon_api03.png}" alt="API 활용기업">
<div class="api_num">
<p th:text="${approvedOrgCount}"></p>
<span>API 활용 기업</span>
</div>
</li>
</ul>
</div>
</div>
</section>
</div>
<!-- API 활용 현황 섹션 -->
<section class="api-stats-section">
<div class="container">
<div class="section-header">
<h2 class="section-title">
우리곁의 수많은 서비스들이<br>
<span class="title-highlight">광주은행 API</span>와 함께하고 있습니다
</h2>
</div>
<div class="stats-cards">
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-store"></i>
</div>
<div class="stat-content">
<div class="stat-number" th:attr="data-target=${serviceCount != null ? serviceCount : 0}" th:text="${serviceCount != null ? serviceCount : 0}">0</div>
<div class="stat-label">서비스 이용 수</div>
<div class="stat-description">다양한 분야의 서비스가 활용중</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-exchange-alt"></i>
</div>
<div class="stat-content">
<div class="stat-number" th:attr="data-target=${totalApiCount != null ? totalApiCount : 0}" th:text="${totalApiCount != null ? totalApiCount : 0}">0</div>
<div class="stat-label">API 이용 건수</div>
<div class="stat-description">일일 평균 API 호출 수</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-building"></i>
</div>
<div class="stat-content">
<div class="stat-number" th:attr="data-target=${approvedOrgCount != null ? approvedOrgCount : 0}" th:text="${approvedOrgCount != null ? approvedOrgCount : 0}">0</div>
<div class="stat-label">API 활용 기업</div>
<div class="stat-description">혁신적인 파트너사와 함께</div>
</div>
</div>
</div>
</div>
</section>
</th:block>
<div id="main_bg"></div>
</section>
</body>
<th:block layout:fragment="contentScript">
<script>
// 숫자 카운트업 애니메이션
document.addEventListener('DOMContentLoaded', function() {
const observerOptions = {
threshold: 0.5,
rootMargin: '0px 0px -100px 0px'
};
const animateNumbers = (entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const numbers = entry.target.querySelectorAll('.stat-number');
numbers.forEach(number => {
const target = parseInt(number.getAttribute('data-target'));
const duration = 2000; // 2초
const increment = target / (duration / 16);
let current = 0;
const updateNumber = () => {
current += increment;
if (current < target) {
number.textContent = Math.floor(current).toLocaleString('ko-KR');
requestAnimationFrame(updateNumber);
} else {
number.textContent = target.toLocaleString('ko-KR');
}
};
updateNumber();
});
observer.unobserve(entry.target);
}
});
};
const observer = new IntersectionObserver(animateNumbers, observerOptions);
const statsSection = document.querySelector('.api-stats-section');
if (statsSection) {
observer.observe(statsSection);
<script th:inline="javascript">
$(document).ready(function() {
// 기존 success 메시지 처리
var successMsg = [[${success}]];
console.log("Success message:", successMsg);
if (successMsg) {
console.log("Showing alert");
customPopups.showAlert(successMsg);
}
// API 검색 기능
const searchInput = document.getElementById('apiSearchInput');
const searchForm = document.querySelector('.search-form');
const hashtags = document.querySelectorAll('.hashtag');
// 이메일 인증 체크
const emailVerificationRequired = [[${session.emailVerificationRequired}]];
if (emailVerificationRequired) {
const emailVerifyMsg = /*[[${session.success}]]*/ '';
const redirectUrl = /*[[${session.redirectUrl}]]*/ '';
// 해시태그 클릭 이벤트
hashtags.forEach(tag => {
tag.addEventListener('click', function(e) {
e.preventDefault();
const searchTerm = this.getAttribute('data-search');
searchInput.value = searchTerm;
// 폼 제출
if (searchForm) {
searchForm.submit();
console.log('emailVerifyMsg:', emailVerifyMsg);
console.log('redirectUrl:', redirectUrl);
customPopups.showAlert(emailVerifyMsg);
$('#customAlertOkButton').off('click.custom').on('click.custom', function () {
customPopups.hideAlert('#customAlert');
window.location.href = redirectUrl;
});
return; // 다른 체크 중단
}
// 장기미사용 계정 체크
const dormantAccount = [[${session.dormantAccount}]];
if (dormantAccount) {
const dormantMsg = /*[[${session.success}]]*/ '';
const redirectUrl = /*[[${session.redirectUrl}]]*/ '';
console.log('dormantMsg:', dormantMsg);
console.log('redirectUrl:', redirectUrl);
customPopups.showAlert(dormantMsg);
$('#customAlertOkButton').off('click.custom').on('click.custom', function () {
customPopups.hideAlert('#customAlert');
window.location.href = redirectUrl;
});
return; // 다른 체크 중단
}
// 비밀번호 만료 체크 추가
const passwordExpired = [[${session.passwordExpired}]];
if (passwordExpired) {
const pwdExpiredMsg = /*[[${session.success}]]*/ '';
const formattedMsg = pwdExpiredMsg.replace(/\n/g, '<br>');
const redirectUrl = /*[[${session.redirectUrl}]]*/ '';
console.log('pwdExpiredMsg:', pwdExpiredMsg);
console.log('redirectUrl:', redirectUrl);
$('#customAlertMessage').html(formattedMsg);
$('#customAlert').css('display','flex');
$('#customAlertOkButton').off('click.custom').on('click.custom', function () {
customPopups.hideAlert('#customAlert');
window.location.href = redirectUrl;
});
}
});
</script>
<script>
function setActive(element) {
// Remove 'active' class from all links
document.querySelectorAll('.keyword_list a').forEach(el => {
el.classList.remove('active');
});
// Add 'active' class to clicked link
element.classList.add('active');
}
document.addEventListener('DOMContentLoaded', (event) => {
const ITEMS_PER_PAGE = 4;
let isExpanded = false;
// 더보기 버튼 초기화
function initMoreButton() {
const mobileMoreBtn = document.querySelector('.more_type.m-only .showmore');
const pcMoreBtn = document.querySelector('.more_type.pc-only .showmore');
const apiInfoBoxes = document.querySelectorAll('.api_info_box');
// 5개 미만이면 더보기 버튼 숨기기
const mobileMoreContainer = document.querySelector('.more_type.m-only');
const pcMoreContainer = document.querySelector('.more_type.pc-only');
console.log('더보기 버튼 초기화:', {
항목개수: apiInfoBoxes.length,
보여질상태: apiInfoBoxes.length >= 5 ? 'block' : 'none'
});
// 더보기 버튼 표시 여부 설정
if (apiInfoBoxes.length < 5) {
mobileMoreContainer.style.setProperty('display', 'none', 'important');
pcMoreContainer.style.setProperty('display', 'none', 'important');
} else {
mobileMoreContainer.style.setProperty('display', 'block', 'important');
pcMoreContainer.style.setProperty('display', 'block', 'important');
}
// 초기 상태 설정
updateApiListVisibility();
// 더보기 버튼 이벤트 리스너 설정
[mobileMoreBtn, pcMoreBtn].forEach(btn => {
if (btn) {
btn.removeEventListener('click', handleMoreClick);
btn.addEventListener('click', handleMoreClick);
}
});
}
function handleMoreClick(e) {
e.preventDefault();
console.log('더보기 버튼 클릭');
isExpanded = !isExpanded; // 상태 토글
// 버튼 이미지/텍스트 변경
const mobileMoreBtn = document.querySelector('.more_type.m-only .showmore img');
const pcMoreBtn = document.querySelector('.more_type.pc-only .showmore img');
if (mobileMoreBtn) {
mobileMoreBtn.alt = isExpanded ? "접기 버튼" : "더보기 버튼";
}
if (pcMoreBtn) {
pcMoreBtn.alt = isExpanded ? "접기 버튼" : "더보기 버튼";
}
updateApiListVisibility();
}
// API 리스트 표시 상태 업데이트
function updateApiListVisibility() {
const apiInfoBoxes = document.querySelectorAll('.api_info_box');
const endIndex = isExpanded ? apiInfoBoxes.length : ITEMS_PER_PAGE;
console.log('업데이트 - API 목록:', {
총개수: apiInfoBoxes.length,
표시상태: isExpanded ? '전체표시' : '일부표시',
표시할개수: endIndex
});
apiInfoBoxes.forEach((box, index) => {
box.style.display = index < endIndex ? 'block' : 'none';
});
}
// HTMX 로드 완료 이벤트 리스너
document.body.addEventListener('htmx:afterSwap', function(evt) {
if (evt.detail.target.id === 'api_spec_list') {
console.log('HTMX 컨텐츠 로드됨 - 페이지 초기화');
isExpanded = false;
initMoreButton();
}
});
document.body.addEventListener('htmx:afterSettle', function(evt) {
const apiListItems = document.querySelectorAll('.api_list');
apiListItems.forEach(function(item) {
item.removeEventListener('click',null);
item.addEventListener('click', function() {
const href = this.getAttribute('data-href');
if (href) {
window.location.href = href;
}
});
});
const apiItems = document.querySelectorAll('.api_info_box');
apiItems.forEach(function(item) {
item.removeEventListener('click',null);
item.addEventListener('click', function() {
const href = this.getAttribute('data-href');
if (href) {
window.location.href = href;
}
});
});
});
// 초기 실행
console.log('초기 실행');
initMoreButton();
// All 카테고리를 기본으로 active 설정
document.querySelector('.keyword_list li:first-child a').classList.add('active');
const apiListItems = document.querySelectorAll('.api_list');
apiListItems.forEach(function(item) {
item.addEventListener('click', function() {
const href = this.getAttribute('data-href');
if (href) {
window.location.href = href;
}
});
});
const apiItems = document.querySelectorAll('.api_info_box');
apiItems.forEach(function(item) {
item.removeEventListener('click',null);
item.addEventListener('click', function() {
const href = this.getAttribute('data-href');
if (href) {
window.location.href = href;
}
});
});
G
const hashtagItems = document.querySelectorAll('.hashtag ul li a');
const searchBox = document.getElementById('search-box');
hashtagItems.forEach(function(item) {
item.addEventListener('click', function(e) {
e.preventDefault();
// Remove the # symbol if present and set the text in search box
const searchText = this.textContent.replace('#', '').trim();
searchBox.value = searchText;
document.getElementById("searchForm").submit();
});
});
const infoItems = document.querySelectorAll('.api_info ul li');
infoItems.forEach(function (item) {
item.style.cursor = 'pointer';
item.addEventListener('click', function() {
const link = this.querySelector('a');
if (link) {
location.href = link.getAttribute('href');
}
});
});
});
</script>
</th:block>
@@ -0,0 +1,232 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_base_layout}">
<body>
<section layout:fragment="contentFragment" class="content">
<div class="content_wrap">
<!-- 타이틀 -->
<div class="sub_title2" id="title-verification-email">
<h2 class="title add5">이메일 인증</h2>
</div>
<div class="inner i_cs10 h_inner2">
<div class="form_type">
<form id="verificationEmailForm" role="form" name="verificationEmailForm" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<input type="hidden" id="userId" name="userId" th:value="${userId}">
<!-- 이메일 주소 표시 (readonly) -->
<div class="info1 p_top2">
<p class="title w_tit">
<span class="dot">이메일 주소</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4">
<input type="email" id="email" name="email" class="common_input_type_1"
th:value="${email}" readonly style="background-color: #f5f5f5;">
</div>
</div>
<p class="info_txt" style="margin-top: 10px; color: #666;">
위 이메일 주소로 인증코드가 발송됩니다.
</p>
</div>
<!-- 인증코드 받기 버튼 -->
<div class="info1 pt28">
<div class="btn_check" style="width: 100%;">
<a class="common_btn_type_2 cbt btn_send_code" style="width: 100%;"><span>인증코드 받기</span></a>
</div>
</div>
<!-- 인증번호 입력 -->
<div class="info1 pt28 auth-code-container" id="authCodeContainer" style="display: none;">
<p class="title w_tit">
<span class="dot">인증코드 입력</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp5">
<div class="certify_box">
<input type="text" id="authCode" class="common_input_type_1 w_input"
placeholder="이메일로 받은 인증코드를 입력하세요" maxlength="6" required>
<span class="certify_time" id="certify_time">05:00</span>
</div>
</div>
<div class="btn_check">
<a class="common_btn_type_2 cbt btn_verify_code"><span>인증코드 확인</span></a>
</div>
</div>
</div>
<!-- 제출 버튼 -->
<div class="btn_application m_top">
<a type="button" class="common_btn_type_1 gray btn_cancel"><span>취소</span></a>
<!-- <div class="btn_gap"></div>-->
<!-- <button type="button" class="common_btn_type_1 btn_complete" disabled><span>인증 완료</span></button>-->
</div>
</form>
</div>
</div>
</div>
</section>
</body>
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
$(document).ready(function() {
let timerInterval;
let resendTimerInterval;
let isVerified = false;
// CSRF 토큰 설정
const csrfHeaderName = /*[[${_csrf.headerName}]]*/ 'X-CSRF-TOKEN';
const csrfToken = /*[[${_csrf.token}]]*/ '';
// 서버 설정값
const resendLimitSeconds = /*[[${@environment.getProperty('portal.auth.resend_limit_seconds', '60')}]]*/ 60;
// 취소 버튼 클릭 시 메인 페이지로 이동
$('.btn_cancel').on('click', function() {
if (confirm('이메일 인증을 취소하고 메인 페이지로 이동하시겠습니까?')) {
location.href = '/';
}
});
// 타이머 시작 함수 (인증 코드 유효 시간)
function startTimer(duration) {
let timer = duration;
clearInterval(timerInterval);
timerInterval = setInterval(function () {
let minutes = parseInt(timer / 60, 10);
let seconds = parseInt(timer % 60, 10);
minutes = minutes < 10 ? "0" + minutes : minutes;
seconds = seconds < 10 ? "0" + seconds : seconds;
$('#certify_time').text(minutes + ":" + seconds);
if (--timer < 0) {
clearInterval(timerInterval);
$('#certify_time').text("00:00").css('color', 'red');
customPopups.showAlert("인증 시간이 만료되었습니다. 인증코드를 다시 받아주세요.");
}
}, 1000);
}
// 재발송 타이머 시작 함수
function startResendTimer() {
let resendTimer = resendLimitSeconds;
const $button = $('.btn_send_code');
clearInterval(resendTimerInterval);
$button.prop('disabled', true);
resendTimerInterval = setInterval(function () {
if (resendTimer > 0) {
$button.find('span').text('재발송 가능 (' + resendTimer + '초)');
resendTimer--;
} else {
clearInterval(resendTimerInterval);
$button.prop('disabled', false).find('span').text('인증코드 재발송');
}
}, 1000);
}
// 인증코드 받기 버튼 클릭
$('.btn_send_code').on('click', function(e) {
e.preventDefault();
const email = $('#email').val();
const $button = $(this);
$button.prop('disabled', true).find('span').text('발송 중...');
$.ajax({
url: '/mypage/send-verification-code',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ email: email }),
headers: {
[csrfHeaderName]: csrfToken
},
success: function(response) {
customPopups.showAlert('인증코드가 이메일로 발송되었습니다.');
$('#authCodeContainer').show();
startTimer(300); // 5분 타이머
startResendTimer(); // 재발송 타이머 시작 (1분)
},
error: function(xhr) {
const errorMsg = xhr.responseJSON?.error || '인증코드 발송에 실패했습니다.';
customPopups.showAlert(errorMsg);
$button.prop('disabled', false).find('span').text('인증코드 받기');
}
});
});
// 인증코드 확인 버튼 클릭
$('.btn_verify_code').on('click', function(e) {
e.preventDefault();
const authCode = $('#authCode').val().trim();
const email = $('#email').val();
if (!authCode) {
customPopups.showAlert('인증코드를 입력해주세요.');
return;
}
if (authCode.length !== 6) {
customPopups.showAlert('6자리 인증코드를 입력해주세요.');
return;
}
$.ajax({
url: '/mypage/verify-email-code',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
email: email,
code: authCode
}),
headers: {
[csrfHeaderName]: csrfToken
},
success: function(response) {
clearInterval(timerInterval);
isVerified = true;
customPopups.showAlert('이메일 인증이 완료되었습니다!');
// 알림 확인 후 메인 페이지로 이동
$('#customAlertOkButton').off('click.emailVerify').on('click.emailVerify', function () {
customPopups.hideAlert('#customAlert');
location.href = '/';
});
},
error: function(xhr) {
const errorMsg = xhr.responseJSON?.error || '인증코드가 일치하지 않습니다.';
customPopups.showAlert(errorMsg);
}
});
});
// 인증 완료 버튼 클릭
$('.btn_complete').on('click', function(e) {
e.preventDefault();
if (!isVerified) {
customPopups.showAlert('이메일 인증을 먼저 완료해주세요.');
return;
}
// 메인 페이지로 이동
customPopups.showAlert('이메일 인증이 완료되었습니다. 메인 페이지로 이동합니다.');
setTimeout(function() {
location.href = '/';
}, 1500);
});
});
</script>
</th:block>
</html>
+1
View File
@@ -13,6 +13,7 @@
<package-name>org.apache.log4j.*</package-name>
<package-name>org.apache.commons.logging.*</package-name>
</prefer-application-packages>
<!-- <prefer-web-inf-classes>true</prefer-web-inf-classes>-->
</container-descriptor>
<session-descriptor>