클라이언트 표현 수정 및 내부 사용자 이메일 도메인 설정 추가:
- "앱" -> "클라이언트"로 명칭 일괄 수정 (HTML/DTO/JS) - 내부 사용자 이메일 도메인 동적 설정 로직 및 기본값 추가
This commit is contained in:
@@ -36,12 +36,12 @@ public class ApiKeyRegistrationDTO implements Serializable {
|
||||
private byte[] appIconData;
|
||||
private String appIconContentType;
|
||||
|
||||
@NotBlank(message = "앱 이름을 입력해주세요.")
|
||||
@Length(max = 100, message = "앱 이름은 100자를 초과할 수 없습니다.")
|
||||
@NotBlank(message = "클라이언트 이름을 입력해주세요.")
|
||||
@Length(max = 100, message = "클라이언트 이름은 100자를 초과할 수 없습니다.")
|
||||
private String appName;
|
||||
|
||||
@NotBlank(message = "앱 설명을 입력해주세요.")
|
||||
@Length(max = 500, message = "앱 설명은 500자를 초과할 수 없습니다.")
|
||||
@NotBlank(message = "클라이언트 설명을 입력해주세요.")
|
||||
@Length(max = 500, message = "클라이언트 설명은 500자를 초과할 수 없습니다.")
|
||||
private String appDescription;
|
||||
|
||||
private String callbackUrl;
|
||||
|
||||
@@ -3,9 +3,18 @@ package com.eactive.apim.portal.common.util;
|
||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 사용자 유형 판별 유틸리티
|
||||
* 내부 운영자와 외부 사용자를 구분하는 공통 로직 제공
|
||||
*
|
||||
* 내부 사용자로 인정할 이메일 도메인은 application.yml 의
|
||||
* {@code portal.internal-user.email-domains} 로 관리한다.
|
||||
* 설정이 없으면 {@link #DEFAULT_INTERNAL_EMAIL_DOMAINS} 를 사용한다.
|
||||
*/
|
||||
public final class UserTypeUtil {
|
||||
|
||||
@@ -13,7 +22,47 @@ public final class UserTypeUtil {
|
||||
throw new IllegalStateException("Utility class");
|
||||
}
|
||||
|
||||
private static final String INTERNAL_EMAIL_DOMAIN = "@kjbank.com";
|
||||
/** 설정 미주입 시 사용할 기본 내부 도메인 */
|
||||
private static final List<String> DEFAULT_INTERNAL_EMAIL_DOMAINS =
|
||||
Collections.unmodifiableList(Arrays.asList("@djbank.com", "@unit-test.com"));
|
||||
|
||||
private static volatile List<String> internalEmailDomains = DEFAULT_INTERNAL_EMAIL_DOMAINS;
|
||||
|
||||
/**
|
||||
* 내부 사용자 이메일 도메인 목록 설정.
|
||||
* {@code UserTypeConfigurer} 가 기동 시 application.yml 값으로 주입한다.
|
||||
*
|
||||
* @param domains 도메인 목록 ("@" 접두사 유무 무관, 대소문자 무관)
|
||||
*/
|
||||
public static void setInternalEmailDomains(List<String> domains) {
|
||||
if (domains == null || domains.isEmpty()) {
|
||||
internalEmailDomains = DEFAULT_INTERNAL_EMAIL_DOMAINS;
|
||||
return;
|
||||
}
|
||||
List<String> normalized = new ArrayList<>();
|
||||
for (String domain : domains) {
|
||||
if (StringUtils.isBlank(domain)) {
|
||||
continue;
|
||||
}
|
||||
String value = domain.trim().toLowerCase();
|
||||
if (!value.startsWith("@")) {
|
||||
value = "@" + value;
|
||||
}
|
||||
if (!normalized.contains(value)) {
|
||||
normalized.add(value);
|
||||
}
|
||||
}
|
||||
internalEmailDomains = normalized.isEmpty()
|
||||
? DEFAULT_INTERNAL_EMAIL_DOMAINS
|
||||
: Collections.unmodifiableList(normalized);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 적용 중인 내부 사용자 이메일 도메인 목록
|
||||
*/
|
||||
public static List<String> getInternalEmailDomains() {
|
||||
return internalEmailDomains;
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 사용자가 내부 운영자인지 확인
|
||||
@@ -21,10 +70,28 @@ public final class UserTypeUtil {
|
||||
* @return 내부 운영자 여부
|
||||
*/
|
||||
public static boolean isInternalUser(PortalAuthenticatedUser user) {
|
||||
if (user == null || StringUtils.isEmpty(user.getEmailAddr())) {
|
||||
if (user == null) {
|
||||
return false;
|
||||
}
|
||||
return user.getEmailAddr().toLowerCase().endsWith(INTERNAL_EMAIL_DOMAIN);
|
||||
return isInternalEmail(user.getEmailAddr());
|
||||
}
|
||||
|
||||
/**
|
||||
* 이메일 주소의 호스트(도메인)로 내부 운영자 여부 판별
|
||||
* @param emailAddr 이메일 주소
|
||||
* @return 내부 운영자 여부
|
||||
*/
|
||||
public static boolean isInternalEmail(String emailAddr) {
|
||||
if (StringUtils.isEmpty(emailAddr)) {
|
||||
return false;
|
||||
}
|
||||
String email = emailAddr.trim().toLowerCase();
|
||||
for (String domain : internalEmailDomains) {
|
||||
if (email.endsWith(domain)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@ import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@@ -25,6 +26,20 @@ public class PortalProperties {
|
||||
|
||||
private FileProperties file = new FileProperties();
|
||||
|
||||
private InternalUserProperties internalUser = new InternalUserProperties();
|
||||
|
||||
/**
|
||||
* 내부 사용자(운영자) 판별 설정
|
||||
*/
|
||||
@Data
|
||||
public static class InternalUserProperties {
|
||||
/**
|
||||
* 내부 사용자로 분류할 이메일 도메인 목록.
|
||||
* "@" 접두사는 있어도 되고 없어도 되며, 대소문자를 구분하지 않는다.
|
||||
*/
|
||||
private List<String> emailDomains = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일 업로드 관련 설정
|
||||
*/
|
||||
|
||||
@@ -127,6 +127,15 @@ portal:
|
||||
logging:
|
||||
log-path: /logs/eapim
|
||||
|
||||
# 내부 사용자(운영자) 판별 설정
|
||||
# 로그인 계정 이메일의 호스트가 아래 도메인이면 내부 사용자로 분류한다.
|
||||
# (첨부파일 형식/크기 검증 우회 등 FileService 내부 사용자 컨텍스트에 사용)
|
||||
# "@" 접두사 유무·대소문자 무관
|
||||
internal-user:
|
||||
email-domains:
|
||||
- djbank.com
|
||||
- unit-test.com
|
||||
|
||||
auth-ttl: 300
|
||||
auth:
|
||||
resend_limit_seconds: 30
|
||||
|
||||
@@ -90,18 +90,18 @@
|
||||
<!-- Hidden clientId -->
|
||||
<input type="hidden" th:field="*{clientId}"/>
|
||||
|
||||
<!-- 앱 이름 -->
|
||||
<!-- 클라이언트 이름 -->
|
||||
<div class="s1-field">
|
||||
<label class="s1-label">앱 이름 <span class="s1-required">*</span></label>
|
||||
<label class="s1-label">클라이언트 이름 <span class="s1-required">*</span></label>
|
||||
<input type="text" id="appName" name="appName" th:field="*{appName}" class="s1-input"
|
||||
placeholder="앱 이름을 입력하세요." required maxlength="100">
|
||||
placeholder="클라이언트 이름을 입력하세요." required maxlength="100">
|
||||
</div>
|
||||
|
||||
<!-- 앱 설명 -->
|
||||
<!-- 클라이언트 설명 -->
|
||||
<div class="s1-field">
|
||||
<label class="s1-label">앱 설명 <span class="s1-required">*</span></label>
|
||||
<label class="s1-label">클라이언트 설명 <span class="s1-required">*</span></label>
|
||||
<textarea id="appDescription" name="appDescription" th:field="*{appDescription}" class="s1-textarea"
|
||||
placeholder="앱 설명을 입력하세요." required maxlength="500"></textarea>
|
||||
placeholder="클라이언트 설명을 입력하세요." required maxlength="500"></textarea>
|
||||
<div class="s1-counter-row">
|
||||
<span id="charCounter" class="s1-counter">0 / 500</span>
|
||||
</div>
|
||||
@@ -132,9 +132,9 @@
|
||||
<input type="hidden" id="ipWhitelist" name="ipWhitelist" value="">
|
||||
</div>
|
||||
|
||||
<!-- 앱 아이콘 (선택 옵션 — 최하단 배치, compact) -->
|
||||
<!-- 클라이언트 아이콘 (선택 옵션 — 최하단 배치, compact) -->
|
||||
<div class="s1-field">
|
||||
<label class="s1-label">앱 아이콘</label>
|
||||
<label class="s1-label">클라이언트 아이콘</label>
|
||||
<div class="s1-upload-box s1-upload-box--compact" id="iconDropZone">
|
||||
|
||||
<!-- 기존 파일 ID로 이미지 노출 -->
|
||||
@@ -154,7 +154,7 @@
|
||||
|
||||
<div id="iconPlaceholder" class="s1-upload-inner" th:style="${(apiKeyModification.appIconData != null or apiKeyModification.appIconFileId != null) ? 'display: none;' : 'display: flex;'}">
|
||||
<img th:src="@{/img/icon/img_icon.png}">
|
||||
<p class="s1-upload-title">앱 아이콘 이미지 파일을 업로드 하세요</p>
|
||||
<p class="s1-upload-title">클라이언트 아이콘 이미지 파일을 업로드 하세요</p>
|
||||
<p class="s1-upload-hint">권장 사이즈는 512 * 512 픽셀이며 JPG,PNG,GIF 파일만 등록할 수 있습니다.</p>
|
||||
<button type="button" class="s1-btn-upload"
|
||||
onclick="document.getElementById('appIconFile').click()">
|
||||
@@ -364,14 +364,14 @@
|
||||
|
||||
if (!name) {
|
||||
e.preventDefault();
|
||||
customPopups.showAlert('앱 이름을 입력해주세요.');
|
||||
customPopups.showAlert('클라이언트 이름을 입력해주세요.');
|
||||
document.getElementById('appName').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!desc) {
|
||||
e.preventDefault();
|
||||
customPopups.showAlert('앱 설명을 입력해주세요.');
|
||||
customPopups.showAlert('클라이언트 설명을 입력해주세요.');
|
||||
textarea.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<!-- Progress Steps Card -->
|
||||
<div class="s1-progress-card">
|
||||
<div class="s1-steps">
|
||||
<!-- Step 1 Active: 앱 정보 입력 (document icon) -->
|
||||
<!-- Step 1 Active: 클라이언트 정보 입력 (document icon) -->
|
||||
<div class="s1-step s1-step--active">
|
||||
<div class="s1-step-circle">
|
||||
<svg class="s1-step-svg" width="26" height="26" viewBox="0 0 24 24" fill="none"
|
||||
@@ -44,7 +44,7 @@
|
||||
</svg>
|
||||
</div>
|
||||
<span class="s1-step-num">1단계</span>
|
||||
<span class="s1-step-name">앱 정보 입력</span>
|
||||
<span class="s1-step-name">클라이언트 정보 입력</span>
|
||||
</div>
|
||||
|
||||
<div class="s1-step-line"></div>
|
||||
@@ -100,18 +100,18 @@
|
||||
<form id="registerStep1Form" method="post" th:action="@{/clients/register/step1}"
|
||||
th:object="${apiKeyRegistration}" enctype="multipart/form-data">
|
||||
|
||||
<!-- 앱 이름 -->
|
||||
<!-- 클라이언트 이름 -->
|
||||
<div class="s1-field">
|
||||
<label class="s1-label">앱 이름 <span class="s1-required">*</span></label>
|
||||
<label class="s1-label">클라이언트 이름 <span class="s1-required">*</span></label>
|
||||
<input type="text" id="appName" name="appName" th:field="*{appName}" class="s1-input"
|
||||
placeholder="앱 이름을 입력하세요." required maxlength="100">
|
||||
placeholder="클라이언트 이름을 입력하세요." required maxlength="100">
|
||||
</div>
|
||||
|
||||
<!-- 앱 설명 -->
|
||||
<!-- 클라이언트 설명 -->
|
||||
<div class="s1-field">
|
||||
<label class="s1-label">앱 설명 <span class="s1-required">*</span></label>
|
||||
<label class="s1-label">클라이언트 설명 <span class="s1-required">*</span></label>
|
||||
<textarea id="appDescription" name="appDescription" th:field="*{appDescription}" class="s1-textarea"
|
||||
placeholder="앱 설명을 입력하세요." required maxlength="500"></textarea>
|
||||
placeholder="클라이언트 설명을 입력하세요." required maxlength="500"></textarea>
|
||||
<div class="s1-counter-row">
|
||||
<span id="charCounter" class="s1-counter">0 / 200</span>
|
||||
</div>
|
||||
@@ -150,14 +150,14 @@
|
||||
<input type="hidden" id="ipWhitelist" name="ipWhitelist" value="">
|
||||
</div>
|
||||
|
||||
<!-- 앱 아이콘 (선택 옵션 — 최하단 배치, compact) -->
|
||||
<!-- 클라이언트 아이콘 (선택 옵션 — 최하단 배치, compact) -->
|
||||
<div class="s1-field">
|
||||
<label class="s1-label">앱 아이콘</label>
|
||||
<label class="s1-label">클라이언트 아이콘</label>
|
||||
<div class="s1-upload-box s1-upload-box--compact" id="iconDropZone">
|
||||
<img id="iconPreviewImage" class="s1-preview-img" style="display:none;" alt="미리보기">
|
||||
<div id="iconPlaceholder" class="s1-upload-inner">
|
||||
<img th:src="@{/img/icon/img_icon.png}">
|
||||
<p class="s1-upload-title">앱 아이콘 이미지 파일을 업로드 하세요</p>
|
||||
<p class="s1-upload-title">클라이언트 아이콘 이미지 파일을 업로드 하세요</p>
|
||||
<p class="s1-upload-hint">권장 사이즈는 512 * 512 픽셀이며 JPG,PNG,GIF 파일만 등록할 수 있습니다.</p>
|
||||
<button type="button" class="s1-btn-upload"
|
||||
onclick="document.getElementById('appIconFile').click()">
|
||||
@@ -382,14 +382,14 @@
|
||||
|
||||
if (!name) {
|
||||
e.preventDefault();
|
||||
customPopups.showAlert('앱 이름을 입력해주세요.');
|
||||
customPopups.showAlert('클라이언트 이름을 입력해주세요.');
|
||||
document.getElementById('appName').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!desc) {
|
||||
e.preventDefault();
|
||||
customPopups.showAlert('앱 설명을 입력해주세요.');
|
||||
customPopups.showAlert('클라이언트 설명을 입력해주세요.');
|
||||
textarea.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
<div class="s3-message-wrapper">
|
||||
<h1 class="s3-success-title">클라이언트 신청이 완료되었습니다.</h1>
|
||||
<p class="s3-success-desc">
|
||||
담당자 앱 승인 후 <span class="s3-highlight">[앱 정보]</span> 화면에서
|
||||
담당자의 클라이언트 승인 후 <span class="s3-highlight">[클라이언트 정보]</span> 화면에서
|
||||
<br>
|
||||
<span class="s3-highlight">Client ID와 Client Secret을 확인</span>할 수 있습니다.
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user