전역 토스트 알림 추가 및 UI/로직 개선:
- 전역 토스트 알림용 JS 및 SCSS 추가 (djbToast) - 신규 토스트 연동 레이아웃(js 포함) 및 스타일 확장 대응 - 클라이언트 신청/수정 화면 개선, IP 등록 검증 로직 추가
This commit is contained in:
@@ -64,6 +64,12 @@ public class ApiController {
|
||||
|
||||
Map<String, Object> searchResult = apiSearchFacade.searchApis(new ApiGroupSearch());
|
||||
|
||||
// 상세 타이틀에 노출할 현재 API의 그룹명 세팅(selectDetail은 apiGroupName을 채우지 않음)
|
||||
ApiServiceDTO apiGroup = apiServiceService.findApiServiceByApiId(id);
|
||||
if (apiGroup != null) {
|
||||
api.setApiGroupName(apiGroup.getGroupName());
|
||||
}
|
||||
|
||||
model.addAttribute("apiSpecInfo", api);
|
||||
model.addAttribute("totalApiCount", searchResult.get("totalApiCount"));
|
||||
model.addAttribute("services", searchResult.get("services"));
|
||||
|
||||
@@ -366,7 +366,7 @@ page:
|
||||
name: "개발자 정보"
|
||||
path: "/users/detail"
|
||||
apikey:
|
||||
name: "앱 관리"
|
||||
name: "API 신청 관리"
|
||||
path: "/myapikey"
|
||||
app_request_detail:
|
||||
name: "인증키 신청 상세"
|
||||
|
||||
@@ -6950,6 +6950,79 @@ select.form-control {
|
||||
color: #c3dfea;
|
||||
}
|
||||
|
||||
.djb-toast-container {
|
||||
position: fixed;
|
||||
top: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 12000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
pointer-events: none;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.djb-toast-container {
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
transform: none;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
.djb-toast {
|
||||
pointer-events: auto;
|
||||
min-width: 260px;
|
||||
max-width: 380px;
|
||||
padding: 14px 18px;
|
||||
border-radius: 10px;
|
||||
background: #f4f7fc;
|
||||
color: #1f2328;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
border: 1px solid #e3e8f0;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transform: translateY(-12px);
|
||||
transition: opacity 0.22s ease, transform 0.22s ease;
|
||||
}
|
||||
.djb-toast.is-show {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
.djb-toast {
|
||||
border-left: 4px solid #0049B4;
|
||||
}
|
||||
.djb-toast--info {
|
||||
border-left-color: #0049B4;
|
||||
}
|
||||
.djb-toast--success {
|
||||
border-left-color: #1f883d;
|
||||
}
|
||||
.djb-toast--warn {
|
||||
border-left-color: #bf8700;
|
||||
}
|
||||
.djb-toast--error {
|
||||
border-left-color: #cf222e;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.djb-toast {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
@media (min-width: 769px) {
|
||||
.djb-toast {
|
||||
min-width: 338px;
|
||||
max-width: 494px;
|
||||
padding: 18px 24px;
|
||||
font-size: 18px;
|
||||
border-radius: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -11297,9 +11370,9 @@ body.index-page-body {
|
||||
margin: 8px 0 0;
|
||||
padding: 24px;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #E2E8F0;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
box-shadow: none;
|
||||
}
|
||||
.testbed-app-panel .testbed-app-panel__head {
|
||||
display: flex;
|
||||
@@ -11404,6 +11477,22 @@ body.index-page-body {
|
||||
}
|
||||
}
|
||||
|
||||
/* "기본 정보" 헤더: 좌측 제목 + 우측 "API 사용 신청" 버튼 */
|
||||
.api-basic-info-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* 하단 중앙 "API 사용 신청" 버튼 */
|
||||
.api-apply-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.api-overview-card {
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
@@ -16412,6 +16501,38 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
border-color: #2a69de;
|
||||
background: #f0f7ff;
|
||||
}
|
||||
.step1-wrap .s1-upload-box--compact,
|
||||
.step2-wrap .s1-upload-box--compact,
|
||||
.step3-wrap .s1-upload-box--compact {
|
||||
min-height: 96px;
|
||||
}
|
||||
.step1-wrap .s1-upload-box--compact .s1-upload-inner,
|
||||
.step2-wrap .s1-upload-box--compact .s1-upload-inner,
|
||||
.step3-wrap .s1-upload-box--compact .s1-upload-inner {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 6px 14px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
.step1-wrap .s1-upload-box--compact .s1-upload-inner > img,
|
||||
.step2-wrap .s1-upload-box--compact .s1-upload-inner > img,
|
||||
.step3-wrap .s1-upload-box--compact .s1-upload-inner > img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
.step1-wrap .s1-upload-box--compact .s1-upload-title,
|
||||
.step2-wrap .s1-upload-box--compact .s1-upload-title,
|
||||
.step3-wrap .s1-upload-box--compact .s1-upload-title {
|
||||
font-size: 13px;
|
||||
}
|
||||
.step1-wrap .s1-upload-box--compact .s1-btn-upload,
|
||||
.step2-wrap .s1-upload-box--compact .s1-btn-upload,
|
||||
.step3-wrap .s1-upload-box--compact .s1-btn-upload {
|
||||
height: 34px;
|
||||
padding: 0 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.step1-wrap .s1-upload-inner,
|
||||
.step2-wrap .s1-upload-inner,
|
||||
.step3-wrap .s1-upload-inner {
|
||||
@@ -23924,6 +24045,42 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.signup-guide-v2 .signup-roles-tbd {
|
||||
margin-top: 40px;
|
||||
}
|
||||
.signup-guide-v2 .signup-roles-tbd__title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #140064;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
.signup-guide-v2 .signup-roles-tbd__placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 48px 24px;
|
||||
background: #f8f9fa;
|
||||
border: 1px dashed #e3e8f0;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.signup-guide-v2 .signup-roles-tbd__badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
color: #0049B4;
|
||||
background: #eff9fe;
|
||||
border-radius: 20px;
|
||||
}
|
||||
.signup-guide-v2 .signup-roles-tbd__desc {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
color: #6e7781;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.service-hero {
|
||||
@@ -24765,6 +24922,12 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.statistics-notice-example {
|
||||
flex-basis: 100%; /* flex-wrap 컨테이너에서 두 번째 줄로 강제 */
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.statistics-notice-latest {
|
||||
color: #0049B4;
|
||||
font-weight: 600;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,79 @@
|
||||
/*!
|
||||
* DjbToast — 경량 전역 토스트 알림.
|
||||
* 사용: window.djbToast('메시지') 또는 djbToast('메시지', { type: 'info', duration: 3000 })
|
||||
*
|
||||
* 추가로, 페이지 진입 시 URL 쿼리 `apiApplyToast` 값에 따라 안내 토스트를 자동 노출한다.
|
||||
* - new : 신규 클라이언트 생성 페이지 진입 안내
|
||||
* - existing : 클라이언트 목록(선택) 진입 안내
|
||||
* 토스트 노출 후 쿼리를 정리(replaceState)해 새로고침 시 재노출을 막는다.
|
||||
*/
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
var CONTAINER_ID = "djb-toast-container";
|
||||
|
||||
function ensureContainer() {
|
||||
var el = document.getElementById(CONTAINER_ID);
|
||||
if (!el) {
|
||||
el = document.createElement("div");
|
||||
el.id = CONTAINER_ID;
|
||||
el.className = "djb-toast-container";
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
function showToast(message, opts) {
|
||||
if (!message) return;
|
||||
opts = opts || {};
|
||||
var duration = typeof opts.duration === "number" ? opts.duration : 3200;
|
||||
var type = opts.type || "info";
|
||||
|
||||
var container = ensureContainer();
|
||||
var toast = document.createElement("div");
|
||||
toast.className = "djb-toast djb-toast--" + type;
|
||||
toast.setAttribute("role", "status");
|
||||
toast.innerHTML = String(message); // 호출부에서만 신뢰 문자열 전달
|
||||
|
||||
container.appendChild(toast);
|
||||
// enter 애니메이션
|
||||
requestAnimationFrame(function () { toast.classList.add("is-show"); });
|
||||
|
||||
var remove = function () {
|
||||
toast.classList.remove("is-show");
|
||||
setTimeout(function () {
|
||||
if (toast.parentNode) toast.parentNode.removeChild(toast);
|
||||
}, 250);
|
||||
};
|
||||
var timer = setTimeout(remove, duration);
|
||||
toast.addEventListener("click", function () { clearTimeout(timer); remove(); });
|
||||
}
|
||||
|
||||
global.djbToast = showToast;
|
||||
|
||||
// ── URL 쿼리 기반 자동 안내 토스트 ──
|
||||
var API_APPLY_MESSAGES = {
|
||||
"new": "API 를 사용할 클라이언트 생성을 먼저 진행해주세요.",
|
||||
"existing": "API 를 추가 사용할 클라이언트를 선택해주세요"
|
||||
};
|
||||
|
||||
function handleApiApplyToast() {
|
||||
var params = new URLSearchParams(global.location.search);
|
||||
var key = params.get("apiApplyToast");
|
||||
if (!key || !API_APPLY_MESSAGES[key]) return;
|
||||
|
||||
showToast(API_APPLY_MESSAGES[key], { type: "info" });
|
||||
|
||||
// 쿼리 제거 후 URL 정리 (새로고침 재노출 방지)
|
||||
params.delete("apiApplyToast");
|
||||
var qs = params.toString();
|
||||
var newUrl = global.location.pathname + (qs ? "?" + qs : "") + global.location.hash;
|
||||
try { global.history.replaceState(null, "", newUrl); } catch (e) { /* noop */ }
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", handleApiApplyToast);
|
||||
} else {
|
||||
handleApiApplyToast();
|
||||
}
|
||||
})(window);
|
||||
@@ -53,7 +53,7 @@
|
||||
'op.tryItOut': { en: 'Try it out', ko: '실행해보기' },
|
||||
'op.cancel': { en: 'Cancel', ko: '취소' },
|
||||
'op.execute': { en: 'Execute', ko: '실행' },
|
||||
'op.clear': { en: 'Clear', ko: '지우기' },
|
||||
'op.clear': { en: 'Clear', ko: '응답 지우기' },
|
||||
'op.reset': { en: 'Reset', ko: '초기화' },
|
||||
'op.parameters': { en: 'Parameters', ko: '파라미터' },
|
||||
'op.parameter': { en: 'Parameter', ko: '파라미터' },
|
||||
|
||||
@@ -225,6 +225,18 @@
|
||||
+ '</div>';
|
||||
}
|
||||
|
||||
// Execute 후 응답(또는 검증 실패) 영역으로 자동 스크롤. 성공/검증실패 모두
|
||||
// .djb-response-grid 로 수렴하므로 이 그리드를 뷰포트 상단으로 올린다.
|
||||
function scrollToGrid(op) {
|
||||
setTimeout(function () {
|
||||
var grid = (op || document).querySelector(".djb-response-grid")
|
||||
|| document.querySelector("#swagger-ui .djb-response-grid");
|
||||
if (!grid) return;
|
||||
try { grid.scrollIntoView({ behavior: "smooth", block: "start" }); }
|
||||
catch (e) { grid.scrollIntoView(); }
|
||||
}, 60);
|
||||
}
|
||||
|
||||
// Execute 클릭 시점에 대상 opblock 확정 + (검증) + 타이머 시작 + 빈 그리드 표시.
|
||||
// 캡처 단계라 Swagger(React) 의 실행 핸들러보다 먼저 실행됨 → 검증 실패 시
|
||||
// stopImmediatePropagation 으로 native 실행을 차단하고 에러만 렌더한다.
|
||||
@@ -247,6 +259,7 @@
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
renderValidationErrors(result.errors);
|
||||
scrollToGrid(targetOpblock);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -261,6 +274,7 @@
|
||||
grid.classList.remove("djb-error");
|
||||
grid.innerHTML = renderEmpty();
|
||||
}
|
||||
scrollToGrid(targetOpblock);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -225,6 +225,15 @@
|
||||
withObserverPaused(function () { renderInto(panelEl()); });
|
||||
return;
|
||||
}
|
||||
// Execute(실행) 클릭 시: Swagger 가 요청 DOM/값을 갱신한 뒤 현재 탭을 재렌더.
|
||||
// (검증 실패는 response.js 캡처가 stopImmediatePropagation 으로 차단 → 여기 도달 안 함)
|
||||
// Swagger 의 opblock-body 재렌더 타이밍이 유동적이라 짧게 두 번 갱신한다.
|
||||
var exec = e.target.closest && e.target.closest(".btn.execute");
|
||||
if (exec) {
|
||||
setTimeout(function () { withObserverPaused(function () { renderInto(panelEl()); }); }, 80);
|
||||
setTimeout(function () { withObserverPaused(function () { renderInto(panelEl()); }); }, 300);
|
||||
return;
|
||||
}
|
||||
var copy = e.target.closest && e.target.closest('#djb-snippet-panel [data-action="copy"]');
|
||||
if (copy) {
|
||||
var code = document.querySelector("#djb-snippet-panel .djb-sn-code");
|
||||
|
||||
@@ -41,6 +41,40 @@
|
||||
.swagger-ui .responses-inner > div { display: none !important; }
|
||||
.swagger-ui .live-responses-table { display: none !important; }
|
||||
|
||||
/* ─── 서버 영역(.scheme-container) ─────────────────────────────────────────────
|
||||
"서버" 셀렉트박스(라벨+select)는 숨긴다 — 요청 URL 은 spec servers[0] 값을 그대로
|
||||
사용하므로 UI 만 제거해도 호출에는 영향 없음. 대신 이 영역 안으로 "앱 선택" 패널
|
||||
(#appsWrap)을 JS 로 이관(relocate)해 배치한다. */
|
||||
.swagger-ui .scheme-container .servers-title,
|
||||
.swagger-ui .scheme-container .servers,
|
||||
.swagger-ui .scheme-container .schemes-server-container { display: none !important; }
|
||||
|
||||
/* 앱 선택(#appsWrap)과 인증(.auth-wrapper) 을 한 행에 8:2 로 배치 */
|
||||
.swagger-ui .scheme-container .schemes {
|
||||
flex-wrap: nowrap;
|
||||
align-items: stretch;
|
||||
gap: 16px;
|
||||
}
|
||||
.swagger-ui .scheme-container #appsWrap {
|
||||
order: 1;
|
||||
flex: 8 1 0;
|
||||
min-width: 0; /* select 축약(truncate) 위해 shrink 허용 */
|
||||
margin: 0;
|
||||
}
|
||||
.swagger-ui .scheme-container .auth-wrapper {
|
||||
order: 2;
|
||||
flex: 2 1 0;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
justify-content: stretch;
|
||||
}
|
||||
.swagger-ui .scheme-container .auth-wrapper .authorize {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
/* ─── 상시 요청 스니펫 패널 (PoC overlay.css #djb-snippet-panel) ─────────────── */
|
||||
.swagger-ui #djb-snippet-panel {
|
||||
margin: 16px;
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// ── DjbToast (전역 토스트 알림) ────────────────────────────────
|
||||
// js/djb/toast.js 와 연동. 화면 우하단 스택 노출.
|
||||
|
||||
.djb-toast-container {
|
||||
position: fixed;
|
||||
top: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 12000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
pointer-events: none;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
transform: none;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
.djb-toast {
|
||||
pointer-events: auto;
|
||||
min-width: 260px;
|
||||
max-width: 380px;
|
||||
padding: 14px 18px;
|
||||
border-radius: 10px;
|
||||
background: #f4f7fc;
|
||||
color: #1f2328;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
border: 1px solid #e3e8f0;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
cursor: pointer;
|
||||
|
||||
opacity: 0;
|
||||
transform: translateY(-12px);
|
||||
transition: opacity 0.22s ease, transform 0.22s ease;
|
||||
|
||||
&.is-show {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
// 좌측 강조 바
|
||||
border-left: 4px solid #0049B4;
|
||||
|
||||
&--info { border-left-color: #0049B4; }
|
||||
&--success { border-left-color: #1f883d; }
|
||||
&--warn { border-left-color: #bf8700; }
|
||||
&--error { border-left-color: #cf222e; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
// 데스크탑: 크기 약 30% 확대 (가독성)
|
||||
@media (min-width: 769px) {
|
||||
min-width: 338px;
|
||||
max-width: 494px;
|
||||
padding: 18px 24px;
|
||||
font-size: 18px;
|
||||
border-radius: 13px;
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@
|
||||
@use 'components/accordion' as *;
|
||||
@use 'components/page-title-banner' as *;
|
||||
@use 'components/alerts' as *;
|
||||
@use 'components/toast' as *;
|
||||
@use 'components/pagination' as *;
|
||||
@use 'components/breadcrumb' as *;
|
||||
@use 'components/test-env-notice' as *;
|
||||
|
||||
@@ -726,9 +726,9 @@
|
||||
margin: $spacing-sm 0 0;
|
||||
padding: $spacing-lg;
|
||||
background: $white;
|
||||
border: 1px solid $border-gray;
|
||||
border: none;
|
||||
border-radius: $border-radius-lg;
|
||||
box-shadow: $shadow-sm;
|
||||
box-shadow: none;
|
||||
|
||||
.testbed-app-panel__head {
|
||||
display: flex;
|
||||
@@ -846,6 +846,22 @@
|
||||
}
|
||||
|
||||
// API Overview Card (Flat Style)
|
||||
/* "기본 정보" 헤더: 좌측 제목 + 우측 "API 사용 신청" 버튼 */
|
||||
.api-basic-info-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $spacing-md;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* 하단 중앙 "API 사용 신청" 버튼 */
|
||||
.api-apply-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: $spacing-xl;
|
||||
}
|
||||
|
||||
.api-overview-card {
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
|
||||
@@ -25,6 +25,12 @@
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.statistics-notice-example {
|
||||
flex-basis: 100%; /* flex-wrap 컨테이너에서 두 번째 줄로 강제 */
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.statistics-notice-latest {
|
||||
color: #0049B4;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -3249,6 +3249,34 @@ input[type="checkbox"]:checked+.custom-checkbox {
|
||||
}
|
||||
}
|
||||
|
||||
// 선택 옵션 필드용 compact 변형: 높이 축소 + 가로 배치
|
||||
.s1-upload-box--compact {
|
||||
min-height: 96px;
|
||||
|
||||
.s1-upload-inner {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 6px 14px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.s1-upload-inner > img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.s1-upload-title {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.s1-btn-upload {
|
||||
height: 34px;
|
||||
padding: 0 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.s1-upload-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -554,6 +554,48 @@ $service-card-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgb
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
// 권한별 이용 가능 서비스 (TBD placeholder)
|
||||
.signup-roles-tbd {
|
||||
margin-top: 40px;
|
||||
|
||||
&__title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #140064;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
&__placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 48px 24px;
|
||||
background: #f8f9fa;
|
||||
border: 1px dashed #e3e8f0;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
color: #0049B4;
|
||||
background: #eff9fe;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
color: #6e7781;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<!-- Header -->
|
||||
<div class="api-market-header">
|
||||
<div class="api-market-title">
|
||||
<h1>서비스</h1>
|
||||
<h1 th:text="${apiSpecInfo.apiGroupName}">그룹명</h1>
|
||||
<h2 th:text="${apiSpecInfo.apiName}">API 이름</h2>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,8 +69,9 @@
|
||||
<div class="tab-content" th:classappend="${activeTab == 'testbed'} ? '' : 'active'" id="api-info-tab">
|
||||
<!-- API Overview Card (Merged with Additional Information) -->
|
||||
<div class="api-overview-card">
|
||||
<div class="org-section-header org-section-header--agreement">
|
||||
<div class="org-section-header org-section-header--agreement api-basic-info-header">
|
||||
<h3>기본 정보</h3>
|
||||
<button type="button" class="btn-action-primary md api-apply-btn">API 사용 신청</button>
|
||||
</div>
|
||||
<div class="api-overview-header">
|
||||
<div class="api-method-badge" th:text="${apiSpecInfo.apiMethod}">GET</div>
|
||||
@@ -142,6 +143,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 하단 중앙 API 사용 신청 -->
|
||||
<div class="api-apply-footer">
|
||||
<button type="button" class="btn-action-primary api-apply-btn">API 사용 신청</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Testbed Tab Content -->
|
||||
@@ -364,6 +370,8 @@
|
||||
onComplete: function() {
|
||||
// 단일 API 페이지 — 상시 요청 스니펫 패널 마운트
|
||||
setTimeout(function() { try { window.DjbSwaggerSnippetPanel.mount({ rootSel: '#swagger-ui' }); } catch (e) { console.error(e); } }, 50);
|
||||
// "앱 선택" 패널을 Swagger 서버 영역(.scheme-container) 안으로 이관
|
||||
setTimeout(function() { try { relocateAppPanel(); } catch (e) { console.error(e); } }, 50);
|
||||
},
|
||||
showMutatedRequest: false,
|
||||
validatorUrl: '',
|
||||
@@ -411,10 +419,80 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ===== API 사용 신청: 회원 구분별 분기 (API 신청 절차) =====
|
||||
// 비회원/개인회원 → 법인회원 안내 팝업 → 회원가입 안내 이동
|
||||
// 법인이용자 → 클라이언트 현황: 신규(앱 0건)→신규 클라이언트 / 기존(앱 있음)→클라이언트 관리
|
||||
function requestApiUse() {
|
||||
fetch(/*[[@{/djb/testbed/auth/context}]]*/ '/djb/testbed/auth/context')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (ctx) {
|
||||
const reason = ctx && ctx.reason;
|
||||
if (reason === 'ANONYMOUS' || reason === 'INDIVIDUAL_USER') {
|
||||
const go = function () { window.location.href = /*[[@{/service/guide}]]*/ '/service/guide'; };
|
||||
// custom-popups.js 는 `const customPopups`(전역 렉시컬) 로 노출 — window 프로퍼티 아님.
|
||||
if (typeof customPopups !== 'undefined' && customPopups.showAlert) {
|
||||
customPopups.showAlert(
|
||||
'API 사용 신청은 법인회원(법인관리자·법인개발자)만 이용할 수 있습니다.<br>회원 가입 안내 페이지로 이동합니다.',
|
||||
go
|
||||
);
|
||||
} else { go(); }
|
||||
return;
|
||||
}
|
||||
// 법인이용자: 보유 클라이언트 유무로 신규/기존 분기 (이동 페이지에서 Toast 안내)
|
||||
const hasClients = ctx && ctx.credentials && ctx.credentials.length > 0;
|
||||
if (hasClients) {
|
||||
// 기존 → 클라이언트 목록(선택) + Toast
|
||||
window.location.href = /*[[@{/myapikey(apiApplyToast='existing')}]]*/ '/myapikey?apiApplyToast=existing';
|
||||
return;
|
||||
}
|
||||
// 신규(클라이언트 0) → 생성 필요 안내 확인 후 이동
|
||||
const goNew = function () {
|
||||
window.location.href = /*[[@{/myapikey/register/step1(clear=true,apiApplyToast='new')}]]*/ '/myapikey/register/step1?clear=true&apiApplyToast=new';
|
||||
};
|
||||
if (typeof customPopups !== 'undefined' && customPopups.showConfirm) {
|
||||
customPopups.showConfirm(
|
||||
'API 를 이용하려면 신규 클라이언트 생성이 필요합니다.<br>클라이언트 생성 페이지로 이동하시겠습니까?',
|
||||
function (ok) { if (ok) goNew(); }
|
||||
);
|
||||
} else { goNew(); }
|
||||
})
|
||||
.catch(function (e) {
|
||||
console.error('API 사용 신청 컨텍스트 조회 실패', e);
|
||||
window.location.href = /*[[@{/myapikey}]]*/ '/myapikey';
|
||||
});
|
||||
}
|
||||
Array.prototype.forEach.call(document.querySelectorAll('.api-apply-btn'), function (btn) {
|
||||
btn.addEventListener('click', requestApiUse);
|
||||
});
|
||||
|
||||
// ===== DJPGPT0001: 앱 인증 정보 자동 주입 =====
|
||||
const DEFAULT_TOKEN_API_ID = 'default-token-api-spec';
|
||||
const appsSelect = document.getElementById('apps');
|
||||
|
||||
// "앱 선택" 패널(#appsWrap)을 Swagger 서버 영역(.scheme-container) 안으로 이관.
|
||||
// 서버 셀렉트박스는 CSS 로 숨기고, 그 자리에 앱 선택을 배치한다.
|
||||
// SwaggerUI 가 scheme-container 를 재렌더하면 노드가 이탈할 수 있어 observer 로 재고정.
|
||||
function relocateAppPanel() {
|
||||
const panel = document.getElementById('appsWrap');
|
||||
if (!panel) return;
|
||||
const scheme = document.querySelector('#swagger-ui .scheme-container');
|
||||
if (!scheme) return;
|
||||
const host = scheme.querySelector('.schemes') || scheme;
|
||||
if (panel.parentNode !== host) host.appendChild(panel);
|
||||
}
|
||||
// scheme-container 재렌더 대비: #swagger-ui 변경 감지 시 재이관(멱등)
|
||||
(function watchAppPanelRelocation() {
|
||||
const root = document.getElementById('swagger-ui');
|
||||
if (!root) return;
|
||||
const obs = new MutationObserver(function () {
|
||||
const panel = document.getElementById('appsWrap');
|
||||
const host = document.querySelector('#swagger-ui .scheme-container .schemes')
|
||||
|| document.querySelector('#swagger-ui .scheme-container');
|
||||
if (panel && host && panel.parentNode !== host) host.appendChild(panel);
|
||||
});
|
||||
obs.observe(root, { childList: true, subtree: true });
|
||||
})();
|
||||
|
||||
function eligibilityMessage(reason) {
|
||||
switch (reason) {
|
||||
case 'ANONYMOUS': return '로그인 후 본인 앱의 인증 정보를 사용할 수 있습니다.';
|
||||
@@ -438,10 +516,17 @@
|
||||
}
|
||||
appsSelect.disabled = false;
|
||||
if (notice) notice.style.display = 'none';
|
||||
// 표시용 client id 축약: 앞 5글자 + "..." (value 는 full 유지 → 인증에 사용)
|
||||
const abbrevId = function (id) {
|
||||
id = id || '';
|
||||
return id.length > 5 ? (id.slice(0, 5) + '...') : id;
|
||||
};
|
||||
(ctx.credentials || []).forEach(function (c) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = c.clientId;
|
||||
opt.textContent = c.clientName ? (c.clientName + ' (' + c.clientId + ')') : c.clientId;
|
||||
opt.textContent = c.clientName
|
||||
? (c.clientName + ' (' + abbrevId(c.clientId) + ')')
|
||||
: abbrevId(c.clientId);
|
||||
appsSelect.appendChild(opt);
|
||||
});
|
||||
})
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<p class="hero-subtitle">세상의 모든 서비스</p>
|
||||
<h2 class="hero-title">DJBank API가<br>함께 합니다.</h2>
|
||||
</div>
|
||||
<a href="#" class="btn-hero-signup">회원가입하기 <i class="bi bi-chevron-right"></i></a>
|
||||
<a href="#" class="btn-hero-signup">회원 가입 안내 <i class="bi bi-chevron-right"></i></a>
|
||||
</div>
|
||||
<div class="hero-image-content">
|
||||
<!-- Inline SVG Tech Illustration -->
|
||||
@@ -327,7 +327,7 @@
|
||||
DJBank API Portal은 기업이 혁신적인 금융 서비스를 쉽고 신속하게 개발하고 구현할 수 있도록 엄선된 API 명세와 직관적인 샌드박스 테스트 환경을 무상으로 지원합니다.
|
||||
</p>
|
||||
<div class="action-buttons">
|
||||
<a href="#" class="action-btn btn-secondary">처음 만나는 DJBank API</a>
|
||||
<a th:href="@{/service/intro}" class="action-btn btn-secondary">처음 만나는 DJBank API</a>
|
||||
<a href="#" class="action-btn btn-primary">피드백 / 개선요청 <i class="bi bi-patch-question"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
<div class="app-management-content">
|
||||
<!-- Header -->
|
||||
<div class="app-management-header">
|
||||
<h2>인증 키 관리</h2>
|
||||
<h2>클라이언트/API 신청 관리</h2>
|
||||
<div sec:authorize="hasRole('ROLE_API_KEY_REQUEST')">
|
||||
<button type="button" class="btn-action-primary md" id="requestApiKey">
|
||||
앱 생성
|
||||
클라이언트 생성
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<div class="step1-wrap">
|
||||
|
||||
<!-- Title -->
|
||||
<h2 class="s1-title">앱 수정</h2>
|
||||
<h2 class="s1-title">클라이언트 정보 - 수정</h2>
|
||||
|
||||
<!-- Progress Steps Card -->
|
||||
<div class="s1-progress-card">
|
||||
@@ -71,7 +71,7 @@
|
||||
</svg>
|
||||
</div>
|
||||
<span class="s1-step-num">3단계</span>
|
||||
<span class="s1-step-name">완료</span>
|
||||
<span class="s1-step-name">클라이언트 신청 완료</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -90,42 +90,6 @@
|
||||
<!-- Hidden clientId -->
|
||||
<input type="hidden" th:field="*{clientId}"/>
|
||||
|
||||
<!-- 앱 아이콘 -->
|
||||
<div class="s1-field">
|
||||
<label class="s1-label">앱 아이콘</label>
|
||||
<div class="s1-upload-box" id="iconDropZone">
|
||||
|
||||
<!-- 기존 파일 ID로 이미지 노출 -->
|
||||
<img class="s1-preview-img" id="iconPreviewImage1"
|
||||
th:if="${apiKeyModification.appIconFileId != null}"
|
||||
th:src="@{/file/download(fileSn=1,fileId=${apiKeyModification.appIconFileId})}"
|
||||
alt="미리보기" style="display: block;">
|
||||
|
||||
<!-- 데이터에서 이미지 노출 -->
|
||||
<img class="s1-preview-img" id="iconPreviewImage2"
|
||||
th:if="${apiKeyModification.appIconData != null and apiKeyModification.appIconFileId == null}"
|
||||
th:src="'data:' + ${apiKeyModification.appIconContentType} + ';base64,' + ${T(java.util.Base64).getEncoder().encodeToString(apiKeyModification.appIconData)}"
|
||||
alt="미리보기" style="display: block;">
|
||||
|
||||
<!-- JS용 이미지 타겟 -->
|
||||
<img id="iconPreviewImageJS" class="s1-preview-img" style="display: none;" alt="미리보기">
|
||||
|
||||
<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-hint">권장 사이즈는 1280 * 12 픽셀이며 JPG,PNG,GIF 파일만 등록할 수 있습니다.</p>
|
||||
<button type="button" class="s1-btn-upload"
|
||||
onclick="document.getElementById('appIconFile').click()">
|
||||
이미지 파일 업로드
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="s1-btn-remove-icon" id="btnRemoveIcon"
|
||||
th:style="${(apiKeyModification.appIconData != null or apiKeyModification.appIconFileId != null) ? 'display: flex;' : 'display: none;'}"
|
||||
onclick="removeAppIcon()">✕</button>
|
||||
<input type="file" id="appIconFile" name="appIcon" accept="image/png,image/jpeg,image/jpg,image/gif" style="display:none;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 앱 이름 -->
|
||||
<div class="s1-field">
|
||||
<label class="s1-label">앱 이름 <span class="s1-required">*</span></label>
|
||||
@@ -168,6 +132,42 @@
|
||||
<input type="hidden" id="ipWhitelist" name="ipWhitelist" value="">
|
||||
</div>
|
||||
|
||||
<!-- 앱 아이콘 (선택 옵션 — 최하단 배치, compact) -->
|
||||
<div class="s1-field">
|
||||
<label class="s1-label">앱 아이콘</label>
|
||||
<div class="s1-upload-box s1-upload-box--compact" id="iconDropZone">
|
||||
|
||||
<!-- 기존 파일 ID로 이미지 노출 -->
|
||||
<img class="s1-preview-img" id="iconPreviewImage1"
|
||||
th:if="${apiKeyModification.appIconFileId != null}"
|
||||
th:src="@{/file/download(fileSn=1,fileId=${apiKeyModification.appIconFileId})}"
|
||||
alt="미리보기" style="display: block;">
|
||||
|
||||
<!-- 데이터에서 이미지 노출 -->
|
||||
<img class="s1-preview-img" id="iconPreviewImage2"
|
||||
th:if="${apiKeyModification.appIconData != null and apiKeyModification.appIconFileId == null}"
|
||||
th:src="'data:' + ${apiKeyModification.appIconContentType} + ';base64,' + ${T(java.util.Base64).getEncoder().encodeToString(apiKeyModification.appIconData)}"
|
||||
alt="미리보기" style="display: block;">
|
||||
|
||||
<!-- JS용 이미지 타겟 -->
|
||||
<img id="iconPreviewImageJS" class="s1-preview-img" style="display: none;" alt="미리보기">
|
||||
|
||||
<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-hint">권장 사이즈는 512 * 512 픽셀이며 JPG,PNG,GIF 파일만 등록할 수 있습니다.</p>
|
||||
<button type="button" class="s1-btn-upload"
|
||||
onclick="document.getElementById('appIconFile').click()">
|
||||
이미지 파일 업로드
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="s1-btn-remove-icon" id="btnRemoveIcon"
|
||||
th:style="${(apiKeyModification.appIconData != null or apiKeyModification.appIconFileId != null) ? 'display: flex;' : 'display: none;'}"
|
||||
onclick="removeAppIcon()">✕</button>
|
||||
<input type="file" id="appIconFile" name="appIcon" accept="image/png,image/jpeg,image/jpg,image/gif" style="display:none;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -204,7 +204,8 @@
|
||||
removeBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
function addIpAddress() {
|
||||
function addIpAddress(refocus) {
|
||||
if (refocus === undefined) refocus = true;
|
||||
const ipInput = document.getElementById('ipWhitelistInput');
|
||||
const ipValue = ipInput.value.trim();
|
||||
|
||||
@@ -227,7 +228,7 @@
|
||||
ipAddresses.push(ipValue);
|
||||
renderIpList();
|
||||
ipInput.value = '';
|
||||
ipInput.focus();
|
||||
if (refocus) ipInput.focus();
|
||||
}
|
||||
|
||||
function removeIpAddress(ip) {
|
||||
@@ -342,6 +343,12 @@
|
||||
}
|
||||
|
||||
// IP input Enter key
|
||||
ipInput.addEventListener('blur', function (e) {
|
||||
const rt = e.relatedTarget;
|
||||
if (rt && rt.closest && rt.closest('.s1-ip-btn--add')) return;
|
||||
if (!ipInput.value.trim()) return; // 빈 값은 무시
|
||||
addIpAddress(false); // blur 경로: 재포커스 안 함
|
||||
});
|
||||
ipInput.addEventListener('keypress', function (e) {
|
||||
if (e.key === 'Enter') { e.preventDefault(); addIpAddress(); }
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<div class="step2-wrap">
|
||||
|
||||
<!-- Title -->
|
||||
<h2 class="s2-title">앱 수정</h2>
|
||||
<h2 class="s2-title">클라이언트 정보 - 수정</h2>
|
||||
|
||||
<!-- Progress Steps Card -->
|
||||
<div class="s1-progress-card">
|
||||
@@ -86,7 +86,7 @@
|
||||
</svg>
|
||||
</div>
|
||||
<span class="s1-step-num">3단계</span>
|
||||
<span class="s1-step-name">앱 수정 완료</span>
|
||||
<span class="s1-step-name">클라이언트 신청 완료</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<div class="step3-wrap">
|
||||
|
||||
<!-- Title -->
|
||||
<h2 class="s3-title">앱 수정</h2>
|
||||
<h2 class="s3-title">클라이언트 정보 - 수정</h2>
|
||||
|
||||
<!-- Progress Steps Card -->
|
||||
<div class="s1-progress-card">
|
||||
@@ -87,7 +87,7 @@
|
||||
</svg>
|
||||
</div>
|
||||
<span class="s1-step-num">3단계</span>
|
||||
<span class="s1-step-name">앱 수정 완료</span>
|
||||
<span class="s1-step-name">클라이언트 신청 완료</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -151,7 +151,7 @@
|
||||
</div>
|
||||
|
||||
<div class="s3-message-wrapper">
|
||||
<h1 class="s3-success-title">앱 수정 신청이 완료되었습니다.</h1>
|
||||
<h1 class="s3-success-title">클라이언트 변경 신청이 완료되었습니다.</h1>
|
||||
<p class="s3-success-desc">
|
||||
<span class="s3-highlight">담당자 승인 후 변경 사항이 적용됩니다.</span>
|
||||
<br>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<div class="step1-wrap">
|
||||
|
||||
<!-- Title -->
|
||||
<h2 class="s1-title">앱생성</h2>
|
||||
<h2 class="s1-title">클라이언트 정보 - 신규</h2>
|
||||
|
||||
<!-- Progress Steps Card -->
|
||||
<div class="s1-progress-card">
|
||||
@@ -84,7 +84,7 @@
|
||||
</svg>
|
||||
</div>
|
||||
<span class="s1-step-num">3단계</span>
|
||||
<span class="s1-step-name">앱 생성 완료</span>
|
||||
<span class="s1-step-name">클라이언트 신청 완료</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -100,27 +100,6 @@
|
||||
<form id="registerStep1Form" method="post" th:action="@{/myapikey/register/step1}"
|
||||
th:object="${apiKeyRegistration}" enctype="multipart/form-data">
|
||||
|
||||
<!-- 앱 아이콘 -->
|
||||
<div class="s1-field">
|
||||
<label class="s1-label">앱 아이콘</label>
|
||||
<div class="s1-upload-box" 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-hint">권장 사이즈는 1280 * 12 픽셀이며 JPG,PNG,GIF 파일만 등록할 수 있습니다.</p>
|
||||
<button type="button" class="s1-btn-upload"
|
||||
onclick="document.getElementById('appIconFile').click()">
|
||||
이미지 파일 업로드
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="s1-btn-remove-icon" id="btnRemoveIcon" style="display:none;"
|
||||
onclick="removeAppIcon()">✕</button>
|
||||
<input type="file" id="appIconFile" name="appIcon" accept="image/png,image/jpeg,image/jpg,image/gif"
|
||||
style="display:none;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 앱 이름 -->
|
||||
<div class="s1-field">
|
||||
<label class="s1-label">앱 이름 <span class="s1-required">*</span></label>
|
||||
@@ -171,6 +150,27 @@
|
||||
<input type="hidden" id="ipWhitelist" name="ipWhitelist" value="">
|
||||
</div>
|
||||
|
||||
<!-- 앱 아이콘 (선택 옵션 — 최하단 배치, compact) -->
|
||||
<div class="s1-field">
|
||||
<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-hint">권장 사이즈는 512 * 512 픽셀이며 JPG,PNG,GIF 파일만 등록할 수 있습니다.</p>
|
||||
<button type="button" class="s1-btn-upload"
|
||||
onclick="document.getElementById('appIconFile').click()">
|
||||
이미지 파일 업로드
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="s1-btn-remove-icon" id="btnRemoveIcon" style="display:none;"
|
||||
onclick="removeAppIcon()">✕</button>
|
||||
<input type="file" id="appIconFile" name="appIcon" accept="image/png,image/jpeg,image/jpg,image/gif"
|
||||
style="display:none;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -202,7 +202,8 @@
|
||||
removeBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
function addIpAddress() {
|
||||
function addIpAddress(refocus) {
|
||||
if (refocus === undefined) refocus = true;
|
||||
const ipInput = document.getElementById('ipWhitelistInput');
|
||||
const ipValue = ipInput.value.trim();
|
||||
|
||||
@@ -225,7 +226,7 @@
|
||||
ipAddresses.push(ipValue);
|
||||
renderIpList();
|
||||
ipInput.value = '';
|
||||
ipInput.focus();
|
||||
if (refocus) ipInput.focus();
|
||||
}
|
||||
|
||||
function removeIpAddress(ip) {
|
||||
@@ -360,6 +361,15 @@
|
||||
if (e.key === 'Enter') { e.preventDefault(); addIpAddress(); }
|
||||
});
|
||||
|
||||
// 포커스 아웃 시 자동 추가/검증 (사용자가 "추가" 버튼을 지나치는 경우 대응).
|
||||
// "추가" 버튼으로 포커스 이동 시엔 버튼 onclick 이 처리하므로 중복 추가 방지.
|
||||
ipInput.addEventListener('blur', function (e) {
|
||||
const rt = e.relatedTarget;
|
||||
if (rt && rt.closest && rt.closest('.s1-ip-btn--add')) return;
|
||||
if (!ipInput.value.trim()) return; // 빈 값은 무시
|
||||
addIpAddress(false); // blur 경로: 재포커스 안 함
|
||||
});
|
||||
|
||||
// Form submit validation
|
||||
form.addEventListener('submit', function (e) {
|
||||
const name = document.getElementById('appName').value.trim();
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<div class="step2-wrap">
|
||||
|
||||
<!-- Title -->
|
||||
<h2 class="s2-title">앱생성</h2>
|
||||
<h2 class="s2-title">클라이언트 정보 - 신규</h2>
|
||||
|
||||
<!-- Progress Steps Card -->
|
||||
<div class="s1-progress-card">
|
||||
@@ -86,7 +86,7 @@
|
||||
</svg>
|
||||
</div>
|
||||
<span class="s1-step-num">3단계</span>
|
||||
<span class="s1-step-name">앱 생성 완료</span>
|
||||
<span class="s1-step-name">클라이언트 신청 완료</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<div class="step3-wrap">
|
||||
|
||||
<!-- Title -->
|
||||
<h2 class="s3-title">앱생성</h2>
|
||||
<h2 class="s3-title">클라이언트 정보 - 신규</h2>
|
||||
|
||||
<!-- Progress Steps Card -->
|
||||
<div class="s1-progress-card">
|
||||
@@ -87,7 +87,7 @@
|
||||
</svg>
|
||||
</div>
|
||||
<span class="s1-step-num">3단계</span>
|
||||
<span class="s1-step-name">앱 생성 완료</span>
|
||||
<span class="s1-step-name">클라이언트 신청 완료</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -151,7 +151,7 @@
|
||||
</div>
|
||||
|
||||
<div class="s3-message-wrapper">
|
||||
<h1 class="s3-success-title">앱 생성이 완료되었습니다.</h1>
|
||||
<h1 class="s3-success-title">클라이언트 신청이 완료되었습니다.</h1>
|
||||
<p class="s3-success-desc">
|
||||
담당자 앱 승인 후 <span class="s3-highlight">[앱 정보]</span> 화면에서
|
||||
<br>
|
||||
|
||||
@@ -190,7 +190,7 @@
|
||||
<div class="dt-actions" style="justify-content: flex-end; gap: 11px; margin-top: 30px;">
|
||||
<a th:href="@{/myapikey}" class="dt-btn-gray">목록</a>
|
||||
<button type="button" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" class="dt-btn-red"
|
||||
th:data-client-id="${apiKey.clientid}" onclick="deleteApiKeyFromButton(this)">인증키 삭제</button>
|
||||
th:data-client-id="${apiKey.clientid}" onclick="deleteApiKeyFromButton(this)">API 이용 해지</button>
|
||||
<a sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" class="dt-btn-blue"
|
||||
th:href="@{/myapikey/modify/step1(clientId=${apiKey.clientid})}">수정</a>
|
||||
</div>
|
||||
|
||||
@@ -206,6 +206,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 회원 구분별 이용 가능 서비스 (TBD) -->
|
||||
<section class="signup-roles-tbd">
|
||||
<h2 class="signup-roles-tbd__title">회원 구분별 이용 가능 서비스</h2>
|
||||
<div class="signup-roles-tbd__placeholder">
|
||||
<span class="signup-roles-tbd__badge">TBD</span>
|
||||
<p class="signup-roles-tbd__desc">회원 구분별 이용 가능 서비스 안내가 준비 중입니다.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="signup-action">
|
||||
<a th:href="@{/signup}" class="btn-action-primary">회원가입하러 가기</a>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
<span th:if="${statsAggregationMinute != null}"
|
||||
th:text="'통계 데이터는 매시 ' + ${statsAggregationMinute} + '분 집계되며, 집계 이전 시간 기준으로 생성됩니다.'">통계 안내</span>
|
||||
<span th:if="${statsAggregationMinute == null}">통계 데이터는 매시 집계되며, 집계 이전 시간 기준으로 생성됩니다.</span>
|
||||
<span class="statistics-notice-example" th:if="${statsAggregationMinute != null}"
|
||||
th:text="'예) 매시 ' + ${statsAggregationMinute} + '분 집계 기준 — 현재 12:30이면 12:00 이전, 12:05이면 11:00 이전에 발생한 거래가 대상입니다.'">예시</span>
|
||||
<span class="statistics-notice-example" th:if="${statsAggregationMinute == null}">예) 현재 12:30이면 12:00 이전, 12:05이면 11:00 이전에 발생한 거래가 대상입니다.</span>
|
||||
<span th:if="${statsLatestTime != null}" class="statistics-notice-latest"
|
||||
th:text="'※ 최신 집계 시각: ' + ${statsLatestTime} + ' 기준'">최신 집계 시각</span>
|
||||
</div>
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
<a th:href="@{/users}"><i class="fas fa-users"></i>개발자 관리</a>
|
||||
</li>
|
||||
<li sec:authorize="hasRole('ROLE_APP')">
|
||||
<a th:href="@{/myapikey}"><i class="fas fa-key"></i>앱 관리</a>
|
||||
<a th:href="@{/myapikey}"><i class="fas fa-key"></i>API 신청 관리</a>
|
||||
<a th:href="@{/webhook}" sec:authorize="hasRole('ROLE_WEBHOOK')"><i class="fas fa-bell"></i>Webhook 관리</a>
|
||||
<a th:href="@{/statistics/api}"><i class="fas fa-chart-bar"></i>이용 통계</a>
|
||||
</li>
|
||||
@@ -239,7 +239,7 @@
|
||||
</button>
|
||||
<ul class="drawer-submenu">
|
||||
<li sec:authorize="hasRole('ROLE_CORP_MANAGER')"><a th:href="@{/users}">개발자 관리</a></li>
|
||||
<li sec:authorize="hasRole('ROLE_APP')"><a th:href="@{/myapikey}">앱 관리</a></li>
|
||||
<li sec:authorize="hasRole('ROLE_APP')"><a th:href="@{/myapikey}">API 신청 관리</a></li>
|
||||
<li sec:authorize="hasRole('ROLE_WEBHOOK')"><a th:href="@{/webhook}">Webhook 관리</a></li>
|
||||
<li sec:authorize="hasRole('ROLE_APP')"><a th:href="@{/statistics/api}">이용 통계</a></li>
|
||||
<li><a th:href="@{/mypage}">내 정보 관리</a></li>
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
<a th:href="@{/myapikey}"
|
||||
th:classappend="${activeMenu == 'apiKey'} ? 'service-nav__item--active' : ''"
|
||||
class="service-nav__item">앱 관리</a>
|
||||
class="service-nav__item">API 신청 관리</a>
|
||||
|
||||
<a th:href="@{/webhook}"
|
||||
th:classappend="${activeMenu == 'webhook'} ? 'service-nav__item--active' : ''"
|
||||
|
||||
@@ -26,5 +26,6 @@
|
||||
<section layout:fragment="pagePopups"></section>
|
||||
<script th:src="@{/js/popup/custom-popups.js}"></script>
|
||||
<script th:src="@{/js/popup/two-factor-auth.js}"></script>
|
||||
<script th:src="@{/js/djb/toast.js}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -27,5 +27,6 @@
|
||||
<section layout:fragment="pagePopups"></section>
|
||||
<script th:src="@{/js/popup/custom-popups.js}"></script>
|
||||
<script th:src="@{/js/popup/two-factor-auth.js}"></script>
|
||||
<script th:src="@{/js/djb/toast.js}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user