fix: 마이페이지 이용 통계 , 내정보 관리 , 비밀전호 변경 & 로그인 반응형 퍼블리싱

This commit is contained in:
hong
2026-07-28 14:30:03 +09:00
parent 2106142d93
commit cdd68f48f0
17 changed files with 1841 additions and 29794 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,22 @@
// -----------------------------------------------------------------------------
// Common Board Header Component Styling
// -----------------------------------------------------------------------------
.board-header {
margin-bottom: 24px;
.board-title {
font-size: 24px;
font-weight: 700;
color: #000000;
margin-bottom: 8px;
line-height: 1.2;
}
.board-desc {
font-size: 14px;
color: #7f8a95;
margin: 0;
line-height: 1.5;
}
}
+1
View File
@@ -45,6 +45,7 @@
@use 'components/test-env-notice' as *;
@use 'components/djb-inquiry-comments' as *;
@use 'components/password-policy' as *;
@use 'components/board-common' as *;
// 5. Page-specific styles
@use 'pages/index' as *;
@@ -8,7 +8,6 @@
.api-statistics-container {
margin: 0 auto;
padding: 48px 0px;
}
// 집계 안내 문구
@@ -34,10 +33,11 @@
// Search Section
.statistics-search-section {
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: space-between;
gap: 12px;
margin-bottom: 30px;
margin-top: 50px;
}
// 일별/월별 모드 토글
@@ -46,6 +46,7 @@
background: #eef1f5;
border-radius: 22px;
padding: 4px;
width: 150px;
}
.mode-tab {
@@ -347,9 +348,10 @@
font-size: 18px;
font-weight: 600;
margin: 0;
margin-bottom: 10px;
svg {
stroke: #fff;
stroke: #1e293b;
}
}
@@ -375,39 +377,89 @@
padding: 0;
}
.statistics-table {
.board-table {
width: 100%;
border-collapse: collapse;
border-top: 1px solid #c4c7c8;
background: #fff;
thead th {
padding: 16px 20px;
text-align: left;
font-weight: 500;
color: #666;
border-bottom: 1px solid #e0e0e0;
background: #fafafa;
}
.board-table-header {
display: flex;
align-items: center;
background: #f9f9f9;
height: 50px;
padding: 0 45px;
border-bottom: 1px solid #dedede;
tbody {
td {
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
color: #333;
@media (max-width: 768px) {
display: none;
}
tr:hover {
background: #f9f9f9;
.header-cell {
font-size: 14px;
font-family: 'Spoqa Han Sans Neo', sans-serif;
font-weight: 500;
color: #000;
text-align: center;
}
}
tfoot .total-row {
background: #e3f2fd;
.board-table-body {
display: flex;
flex-direction: column;
}
td {
padding: 16px 20px;
.board-table-row {
display: flex;
align-items: center;
height: 52px;
padding: 0 45px;
border-bottom: 1px solid #dedede;
transition: background-color 0.2s;
background: #ffffff;
&:hover {
background-color: #f8fbfe;
}
@media (max-width: 768px) {
flex-direction: column;
align-items: flex-start;
height: auto;
padding: 15px;
gap: 10px;
}
.row-cell {
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-family: 'Spoqa Han Sans Neo', sans-serif;
font-weight: 400;
color: #000;
white-space: nowrap;
@media (max-width: 768px) {
justify-content: flex-start;
width: 100% !important;
&::before {
content: attr(data-label);
font-weight: 500;
color: #64748b;
margin-right: 10px;
min-width: 80px;
}
}
}
&.total-row {
background: #f8fbfe;
font-weight: 600;
color: #0049B4;
border-bottom: none;
.row-cell {
color: #0049B4;
}
}
}
}
@@ -440,39 +492,64 @@
}
}
// Progress Bar
.progress-cell {
min-width: 200px;
.api-name-text {
display: inline-block;
vertical-align: middle;
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@media (max-width: 768px) {
max-width: 320px;
}
}
.progress-bar {
// Progress Bar
.col-name {
flex: 2;
justify-content: flex-start !important;
padding-left: 10px;
min-width: 0; // Allows flex child truncation
}
.board-table-header .col-name {
justify-content: center !important;
padding-left: 0;
}
.col-num {
width: 100px;
justify-content: center !important;
}
.col-chart {
width: 180px;
justify-content: center !important;
}
.stats-progress {
width: 150px;
height: 8px;
background: #f1f5f9;
border-radius: 4px;
display: flex;
height: 12px;
background: #e0e0e0;
border-radius: 6px;
overflow: hidden;
}
.progress-success {
background: #4A90D9;
transition: width 0.3s ease;
.stats-empty {
display: flex;
align-items: center;
justify-content: center;
height: 100px;
width: 100%;
color: #64748b;
font-weight: 400;
font-size: 16px;
}
.progress-timeout {
background: #F5C36B;
transition: width 0.3s ease;
}
.progress-failure {
background: #F5A3B5;
transition: width 0.3s ease;
}
// Empty State
.empty-row td {
text-align: center;
padding: 40px 20px;
color: #999;
.progress-cell {
min-width: 200px;
}
// Responsive
@@ -480,6 +557,7 @@
.summary-content {
flex-direction: column;
gap: 30px;
align-items: center;
}
.summary-left,
@@ -487,6 +565,22 @@
.summary-right {
flex: none;
width: 100%;
align-items: center;
justify-content: center;
text-align: center;
}
.summary-info {
padding-left: 0;
}
.app-select-wrapper {
display: flex;
justify-content: center;
}
.summary-detail-list {
align-items: center;
}
.summary-center {
@@ -495,42 +589,101 @@
.summary-right {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 20px;
}
.rate-display {
border-bottom: none;
padding: 0;
}
}
@media (max-width: 768px) {
.api-statistics-container {
padding: 20px 16px;
}
@media (max-width: $breakpoint-sm) {
// .api-statistics-container {
// padding: 20px 16px;
// }
.statistics-summary-card {
padding: 24px 20px;
}
.app-toggle-buttons {
flex-wrap: wrap;
}
.toggle-btn {
flex: 1;
text-align: center;
}
.statistics-table {
thead th,
tbody td,
tfoot td {
padding: 12px 10px;
font-size: 13px;
}
}
.progress-cell {
min-width: 100px;
.statistics-search-section {
flex-direction: column;
align-items: stretch;
gap: 16px;
}
.search-filter-row {
flex-wrap: wrap;
flex-direction: row;
justify-content: space-between;
width: 100%;
}
}
.date-range-picker {
flex: 1;
justify-content: space-between;
}
.date-input {
width: 45%;
font-size: 13px;
padding: 6px 8px;
}
.board-table-header {
display: none !important;
}
.board-table-row {
flex-direction: column;
align-items: stretch;
height: auto;
padding: 10px 14px;
gap: 6px;
border-bottom: 1px solid #e2e8f0;
.row-cell {
display: flex !important;
justify-content: space-between !important;
align-items: center;
width: 100% !important;
text-align: right;
font-size: 13px;
padding: 0 !important;
&::before {
content: attr(data-label);
font-weight: 500;
color: #64748b;
text-align: left;
min-width: 80px;
}
&.col-name {
font-weight: 600;
justify-content: flex-start !important;
text-align: left;
border-bottom: none;
padding-bottom: 0 !important;
margin-bottom: 4px;
display: flex !important;
gap: 10px;
&::before {
// content: "API명" !important;
// display: inline-block !important;
// font-weight: 500;
// color: #64748b;
// min-width: 80px;
display: none;
}
}
}
}
.stats-progress {
width: 100px;
}
}
@@ -1240,6 +1240,36 @@
color: #64748b;
}
.info-notice-box {
background: #ecf0fa;
border-radius: 7px;
padding: 13px 14px 12px;
display: flex;
align-items: center;
gap: 13px;
margin-bottom: 32px;
svg {
width: 24px;
height: 24px;
flex-shrink: 0;
}
p {
color: #4685ef;
font-size: 16px;
font-weight: 500;
margin: 0;
}
&.danger {
background: #fdf3f3;
p {
color: #f4253c;
}
}
}
// Footer Buttons layout
.dt-actions {
display: flex;
+134 -73
View File
@@ -10,12 +10,11 @@
display: flex;
align-items: center;
justify-content: center;
background: #EDF9FE; // Light blue background from Figma
padding: 40px 20px;
padding: 100px 20px;
position: relative;
border-radius: 12px;
margin-top: 60px;
margin-bottom: 60px;
margin-bottom: 50px;
}
.login-container {
@@ -37,60 +36,104 @@
}
}
.login-logo {
width: 90px;
height: 90px;
margin-bottom: 30px;
.login-header {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 40px;
width: 100%;
img {
width: 100%;
height: 100%;
object-fit: contain;
.header-main-row {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-bottom: 12px;
}
}
.login-message {
.login-lock-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
svg {
stroke: #000;
}
}
.login-title {
font-family: $font-family-primary;
font-size: 20px;
font-weight: 400;
font-size: 36px;
font-weight: 700;
color: #000000;
text-align: center;
margin-bottom: 40px;
margin: 0;
line-height: 1;
}
.login-message-sub {
font-family: $font-family-primary;
font-size: 18px;
font-weight: 300;
color: #80878d;
text-align: center;
margin: 0;
line-height: 1.2;
}
.login-form {
width: 100%;
margin-bottom: 0;
.form-group {
margin-bottom: 24px;
margin-bottom: 16px;
position: relative;
&:last-of-type {
margin-bottom: 0;
&.input-with-icon {
.input-icon {
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
svg {
stroke: #767e89;
}
}
.form-input {
padding-left: 56px;
}
}
}
.form-input {
width: 100%;
height: 70px;
padding: 0 32px;
height: 58px;
padding: 0 20px;
font-family: $font-family-primary;
font-size: 16px;
font-weight: 400;
color: #5F666C;
background: #FFFFFF;
border: 1px solid #DDDDDD;
border-radius: 12px;
font-weight: 300;
color: #000000;
background: #fcfcfc;
border: 1px solid #dfdfdf;
border-radius: 5px;
outline: none;
transition: all 0.3s ease;
&::placeholder {
color: #5F666C;
color: #767e89;
}
&:focus {
border-color: #0049B4;
border-color: #1b4ab7;
background: #ffffff;
}
&.error {
@@ -101,17 +144,16 @@
.form-checkbox {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 7px;
margin-top: 24px;
margin-bottom: 22px;
margin-right: 10px;
justify-content: flex-start;
gap: 8px;
margin-top: 14px;
margin-bottom: 30px;
input[type="checkbox"] {
width: 20px;
height: 20px;
border: 1.5px solid #000000;
border-radius: 4px;
width: 14px;
height: 14px;
border: 1px solid #dfdfdf;
border-radius: 2px;
cursor: pointer;
appearance: none;
background: #FFFFFF;
@@ -119,8 +161,8 @@
flex-shrink: 0;
&:checked {
background: #FFFFFF;
border-color: #2C2C2C;
background: #1b4ab7;
border-color: #1b4ab7;
&::after {
content: '';
@@ -128,22 +170,22 @@
left: 50%;
top: 45%;
transform: translate(-50%, -50%) rotate(45deg);
width: 6px;
height: 12px;
border: solid #2C2C2C;
border-width: 0 2px 2px 0;
width: 3px;
height: 6px;
border: solid #ffffff;
border-width: 0 1.5px 1.5px 0;
}
}
}
label {
font-family: $font-family-primary;
font-size: 16px;
font-weight: 400;
color: #000000;
font-size: 12px;
font-weight: 300;
color: #767e89;
cursor: pointer;
user-select: none;
line-height: 24px;
line-height: 1;
white-space: nowrap;
}
}
@@ -151,26 +193,26 @@
.login-button {
width: 100%;
height: 80px;
height: 58px;
padding: 10px;
font-family: $font-family-primary;
font-size: 20px;
font-weight: 700;
font-size: 16px;
font-weight: 500;
color: #FFFFFF;
background: #0049B4;
background: #1b4ab7;
border: none;
border-radius: 12px;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 40px;
line-height: 1;
margin-bottom: 30px;
line-height: 20px;
&:hover {
background: darken(#0049B4, 5%);
background: darken(#1b4ab7, 5%);
}
&:active {
background: darken(#0049B4, 10%);
background: darken(#1b4ab7, 10%);
}
&:disabled {
@@ -179,31 +221,51 @@
}
}
.login-links {
.login-links-container {
display: flex;
justify-content: center;
align-items: center;
gap: 26px;
flex-wrap: wrap;
width: 100%;
a {
.link-btn {
flex: 1;
height: 38px;
display: inline-flex;
align-items: center;
justify-content: center;
background: #ecf0fa;
border: 1px solid #2a69de;
font-family: $font-family-primary;
font-size: 16px;
font-size: 14px;
font-weight: 400;
color: #000000;
color: #2a69de;
text-decoration: none;
transition: color 0.3s ease;
line-height: 1;
transition: all 0.2s ease;
line-height: 20px;
text-align: center;
&.first {
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
border-right: none;
}
&.middle {
border-right: none;
}
&.last {
border-top-right-radius: 8px;
border-bottom-right-radius: 8px;
}
&:hover {
color: #0049B4;
text-decoration: underline;
background: darken(#ecf0fa, 4%);
}
}
.link-separator {
color: #000000;
font-size: 16px;
&:active {
background: darken(#ecf0fa, 8%);
}
}
}
@@ -282,9 +344,8 @@
@include respond-to('sm') {
.login-page {
// Figma: 전체 배경 #ebfbff, 풀 높이
background: #ebfbff;
min-height: calc(100vh - 44px); // 모바일 헤더 높이 제외
padding: 40px 20px;
padding: 100px 20px;
margin-top: 0;
margin-bottom: 0;
border-radius: 0;
@@ -392,4 +453,4 @@
padding: 10px 14px;
font-size: 13px;
}
}
}
@@ -26,13 +26,14 @@
// -----------------------------------------------------------------------------
// Corporate Manager Profile Form Styles
// -----------------------------------------------------------------------------
.corp-manager-wrapper, .password-change-wrapper {
.corp-manager-wrapper,
.password-change-wrapper {
width: 100%;
margin: 0 auto;
padding: 40px 0;
// padding: 40px 0;
.page-outer-title {
font-size: 36px;
font-size: 24px;
font-weight: 700;
color: #000;
margin-bottom: 24px;
@@ -81,12 +82,17 @@
flex-direction: row;
gap: 34px;
> div {
>div {
flex: 1;
display: flex;
flex-direction: column;
gap: 13px;
}
@media (max-width: $breakpoint-sm) {
flex-direction: column;
gap: 26px;
}
}
}
@@ -218,7 +224,7 @@
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 40px;
margin-top: 0;
}
.btn-withdrawal {
@@ -291,13 +297,21 @@
font-weight: 500;
margin: 0;
}
&.danger {
background: #fdf3f3;
p {
color: #f4253c;
}
}
}
.password-rules-list {
list-style-type: disc;
padding-left: 24px;
margin: 20px 0 0 0;
li {
color: #888;
font-size: 16px;
@@ -309,4 +323,4 @@
}
}
}
}
}
@@ -93,7 +93,9 @@
th:if="${notice.noticeType == '4'}">테스트</span>
<span class="notice-type-badge notice-type-badge--normal"
th:if="${notice.noticeType == '1'}">공지</span>
<span th:text="${#strings.replace(#strings.replace(#strings.replace(#strings.replace(notice.noticeSubject, '[장애]', ''), '[점검]', ''), '[테스트]', ''), '[공지]', '')}">공지사항 제목</span>
<span
th:text="${#strings.replace(#strings.replace(#strings.replace(#strings.replace(notice.noticeSubject, '[장애]', ''), '[점검]', ''), '[테스트]', ''), '[공지]', '')}">공지사항
제목</span>
<span class="file-icon" th:if="${notice.fileId != null and !notice.fileId.isEmpty()}">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@@ -39,11 +39,9 @@
<section class="service-content">
<!-- 상단: 최근 작성한 글 목록 -->
<div class="recent-apps">
<div class="recent-apps-header-group">
<p class="recent-apps-title" style="font-size: 24px; font-weight: 700; color: #000; margin-bottom: 8px;">
피드백 / 개선 요청</p>
<p class="recent-apps-subtitle" style="font-size: 14px; color: #7f8a95; margin-bottom: 24px;">최근 최대 3개의 나의
피드백/ 개선 요청을 보여줍니다.</p>
<div class="board-header">
<h2 class="board-title">피드백 / 개선 요청</h2>
<p class="board-desc">최근 최대 3개의 나의 피드백/ 개선 요청을 보여줍니다.</p>
</div>
<ul class="recent-apps-list" th:if="${recentApplications != null and !recentApplications.isEmpty()}">
@@ -73,10 +71,10 @@
<!-- 하단: 신규 작성 폼 카드 -->
<div class="djb-board-write-container">
<!-- Title Bar -->
<div class="djb-board-write-header" style="margin-bottom: 30px;">
<h2 class="title" style="font-size: 24px; font-weight: 700; color: #000; margin-bottom: 8px;">피드백 / 개선 요청
<div class="board-header">
<h2 class="board-title">피드백 / 개선 요청
</h2>
<p class="subtitle" style="font-size: 14px; color: #7f8a95; margin: 0;">DJ Bank은 온라인 비즈니스 혁신을 위한 피드백/개선요청을
<p class="board-desc">DJ Bank은 온라인 비즈니스 혁신을 위한 피드백/개선요청을
환영합니다.</p>
</div>
@@ -1,93 +1,105 @@
<!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/djbank_base_layout}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/djbank_base_layout}">
<body>
<th:block layout:fragment="contentFragment">
<div class="login-page">
<div class="login-container">
<div class="login-card">
<!-- Logo -->
<div class="login-logo">
<img th:src="@{/img/logo/logo_box.png}" alt="DJBank">
</div>
<!-- Header Message -->
<p class="login-message">로그인 하시면 더 많은 서비스를 이용하실 수 있습니다.</p>
<!-- Alert Messages -->
<div th:if="${param.logout}" class="login-alert alert-info">
<i class="fas fa-info-circle"></i>
<span>로그아웃되었습니다.</span>
</div>
<div th:if="${param.expired}" class="login-alert alert-info">
<i class="fas fa-info-circle"></i>
<span>장시간 미사용으로 세션이 만료되어 로그아웃되었습니다. 다시 로그인해주세요.</span>
</div>
<div th:if="${param.forceLogout}" class="login-alert alert-info">
<i class="fas fa-info-circle"></i>
<span>다른 기기 또는 브라우저에서 로그인되어 현재 세션이 종료되었습니다.</span>
</div>
<div th:if="${param.reason == 'auth'}" class="login-alert alert-info">
<i class="fas fa-info-circle"></i>
<span>인증된 사용자만 접근 가능한 페이지입니다. 로그인 후 이용해 주세요.</span>
</div>
<!-- Login Form -->
<form id="loginForm" role="form" name="loginForm" th:action="@{/actionLogin.do}" method="post" class="login-form">
<input type="hidden" id="message" name="message" th:value="${loginMessage}">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<input type="hidden" id="loginId" th:value="${loginId}">
<!-- ID Field -->
<div class="form-group">
<input type="text"
id="id"
name="id"
th:value="${loginId}"
class="form-input"
placeholder="아이디"
required>
<th:block layout:fragment="contentFragment">
<div class="login-page">
<div class="login-container">
<div class="login-card">
<!-- Logo & Title -->
<div class="login-header">
<div class="header-main-row">
<div class="login-lock-icon">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#000"
stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg>
</div>
<h2 class="login-title">로그인</h2>
</div>
<p class="login-message-sub">DJ Bank에 오신걸 환영합니다</p>
</div>
<!-- Password Field -->
<div class="form-group">
<input type="password"
id="password"
name="password"
class="form-input"
placeholder="비밀번호"
required>
<!-- Alert Messages -->
<div th:if="${param.logout}" class="login-alert alert-info">
<i class="fas fa-info-circle"></i>
<span>로그아웃되었습니다.</span>
</div>
<div th:if="${param.expired}" class="login-alert alert-info">
<i class="fas fa-info-circle"></i>
<span>장시간 미사용으로 세션이 만료되어 로그아웃되었습니다. 다시 로그인해주세요.</span>
</div>
<div th:if="${param.forceLogout}" class="login-alert alert-info">
<i class="fas fa-info-circle"></i>
<span>다른 기기 또는 브라우저에서 로그인되어 현재 세션이 종료되었습니다.</span>
</div>
<div th:if="${param.reason == 'auth'}" class="login-alert alert-info">
<i class="fas fa-info-circle"></i>
<span>인증된 사용자만 접근 가능한 페이지입니다. 로그인 후 이용해 주세요.</span>
</div>
<!-- Remember Me Checkbox -->
<div class="form-checkbox">
<input id="checkId" name="checkId" type="checkbox">
<label for="checkId">아이디저장</label>
<!-- Login Form -->
<form id="loginForm" role="form" name="loginForm" th:action="@{/actionLogin.do}" method="post"
class="login-form">
<input type="hidden" id="message" name="message" th:value="${loginMessage}">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<input type="hidden" id="loginId" th:value="${loginId}">
<!-- ID Field -->
<div class="form-group input-with-icon">
<span class="input-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#767e89"
stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
</span>
<input type="text" id="id" name="id" th:value="${loginId}" class="form-input"
placeholder="아이디를 입력해주세요" required>
</div>
<!-- Password Field -->
<div class="form-group input-with-icon">
<span class="input-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#767e89"
stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg>
</span>
<input type="password" id="password" name="password" class="form-input"
placeholder="비밀번호를 입력해주세요" required>
</div>
<!-- Remember Me Checkbox -->
<div class="form-checkbox">
<input id="checkId" name="checkId" type="checkbox">
<label for="checkId">아이디 저장</label>
</div>
</form>
<!-- Login Button -->
<button type="button" class="login-button" id="actionLogin">
로그인
</button>
<!-- Additional Links -->
<div class="login-links-container">
<a th:href="@{/account_recovery(tab='findId')}" class="link-btn first">아이디 찾기</a>
<a th:href="@{/account_recovery(tab='resetPassword')}" class="link-btn middle">비밀번호 초기화</a>
<a th:href="@{/signup}" class="link-btn last">회원가입</a>
</div>
</form>
<!-- Login Button -->
<button type="button" class="login-button" id="actionLogin">
로그인
</button>
<!-- Additional Links -->
<div class="login-links">
<a th:href="@{/account_recovery(tab='findId')}">아이디 찾기</a>
<span class="link-separator">|</span>
<a th:href="@{/account_recovery(tab='resetPassword')}">비밀번호 초기화</a>
<span class="link-separator">|</span>
<a th:href="@{/signup}">회원가입</a>
</div>
<!-- Loading State -->
<div class="login-loading" id="loginLoading">
<div class="spinner"></div>
<!-- Loading State -->
<div class="login-loading" id="loginLoading">
<div class="spinner"></div>
</div>
</div>
</div>
</div>
</div>
</th:block>
</th:block>
</body>
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
@@ -148,8 +160,8 @@
if (data && data.duplicateSession) {
$('#loginLoading').removeClass('active');
var msg = '해당 계정은 이미 다른 곳(' + data.ipAddress + ')에서 접속 중입니다.<br>'
+ '접속 시각: ' + data.loginTime + '<br><br>'
+ '기존 접속을 해제하고 로그인하시겠습니까?';
+ '접속 시각: ' + data.loginTime + '<br><br>'
+ '기존 접속을 해제하고 로그인하시겠습니까?';
customPopups.showConfirm(msg, function (confirmed) {
if (confirmed) {
$('#loginLoading').addClass('active');
@@ -182,7 +194,7 @@
// 세션 만료 대응은 head의 익명 keepalive ping(/api/session/ping)이 담당하고,
// 제출 직전 refreshCsrfAndThen()이 CSRF 토큰 재발급 안전망 역할을 한다.
var successMsg = [[${success}]];
var successMsg = [[${ success }]];
console.log("Success message:", successMsg);
if (successMsg) {
console.log("Showing alert");
@@ -190,13 +202,13 @@
}
// 비밀번호 만료 체크 관련 코드 추가
var successMsg = [[${session.success}]];
var passwordExpired = [[${session.passwordExpired}]];
var emailVerificationRequired = [[${session.emailVerificationRequired}]];
var redirectUrl = [[${session.redirectUrl}]];
var successMsg = [[${ session.success }]];
var passwordExpired = [[${ session.passwordExpired }]];
var emailVerificationRequired = [[${ session.emailVerificationRequired }]];
var redirectUrl = [[${ session.redirectUrl }]];
if (successMsg) {
customPopups.showAlert(successMsg, function() {
customPopups.showAlert(successMsg, function () {
// 비밀번호 만료·미인증(이메일 인증 필요) 시 전용 화면으로 이동
if ((passwordExpired || emailVerificationRequired) && redirectUrl) {
window.location.href = redirectUrl;
@@ -237,7 +249,7 @@
form.classList.add('was-validated');
};
$('#actionLogin').on('click', function(event) {
$('#actionLogin').on('click', function (event) {
event.preventDefault();
actionLogin(event);
});
@@ -252,4 +264,5 @@
});
</script>
</th:block>
</html>
</html>
@@ -25,9 +25,13 @@
<!-- 모든 스타일은 detail-wrap 안에서만 적용 -->
<div class="detail-wrap">
<!-- Title Bar (Figma 및 스크린샷 대응) -->
<div class="common-title-bar">
<h2 class="common-title" th:text="${appRequest.approval != null and appRequest.approval.approvalStatus != null and appRequest.approval.approvalStatus.toString() == 'APPROVED' ? '인증키 정보' : '신청 상세'}">신청 상세</h2>
<span class="common-subtitle" th:text="${appRequest.approval != null and appRequest.approval.approvalStatus != null and appRequest.approval.approvalStatus.toString() == 'APPROVED' ? '등록된 앱의 상세 정보를 확인할 수 있습니다.' : '신청하신 API Key의 상세 정보를 확인할 수 있습니다.'}">신청하신 API Key의 상세 정보를 확인할 수 있습니다.</span>
<div class="board-header">
<h2 class="board-title"
th:text="${appRequest.approval != null and appRequest.approval.approvalStatus != null and appRequest.approval.approvalStatus.toString() == 'APPROVED' ? '인증키 정보' : '신청 상세'}">
신청 상세</h2>
<span class="board-desc"
th:text="${appRequest.approval != null and appRequest.approval.approvalStatus != null and appRequest.approval.approvalStatus.toString() == 'APPROVED' ? '등록된 앱의 상세 정보를 확인할 수 있습니다.' : '신청하신 API Key의 상세 정보를 확인할 수 있습니다.'}">신청하신
API Key의 상세 정보를 확인할 수 있습니다.</span>
</div>
<!-- App Basic Info Card -->
@@ -49,7 +53,7 @@
<div class="dt-app-status-row" th:if="${appRequest.approval != null}">
<span class="dt-status-badge"
th:classappend="${appRequest.approval.approvalStatus.toString() == 'APPROVED' ? 'status-approved' :
(appRequest.approval.approvalStatus.toString() == 'REJECTED' ? 'status-rejected' : 'status-pending')}"
(appRequest.approval.approvalStatus.toString() == 'REJECTED' or appRequest.approval.approvalStatus.toString() == 'REQUESTED' ? 'status-rejected' : 'status-pending')}"
th:text="${appRequest.approval.approvalStatus.description}">
대기중
</span>
@@ -80,7 +84,8 @@
<svg width="20" height="20" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1">
</path>
</svg>
<span>복사</span>
</button>
@@ -176,7 +181,8 @@
</div>
<!-- 승인일시 (approvalDate 가 있을 때만) -->
<div class="dt-row" th:if="${appRequest.approval != null and appRequest.approval.approvalDate != null}">
<div class="dt-row"
th:if="${appRequest.approval != null and appRequest.approval.approvalDate != null}">
<div class="dt-col">
<label class="dt-label">승인일시</label>
<div class="dt-input-group">
@@ -204,11 +210,16 @@
</div>
<!-- 내부 결재 진행중: 취소 불가 안내 -->
<p class="dt-cancel-notice" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
th:if="${appRequest.approval != null and appRequest.approval.approvalStatus != null and
<div class="info-notice-box danger" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" th:if="${appRequest.approval != null and appRequest.approval.approvalStatus != null and
appRequest.approval.approvalStatus.toString() == 'PROCESSING'}">
내부 결재가 진행 중이라 신청을 취소할 수 없습니다. 취소가 필요한 경우 관리자에게 문의해 주세요.
</p>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#f4253c"
stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
<p>내부 결재가 진행 중이라 신청을 취소할 수 없습니다. 취소가 필요한 경우 관리자에게 문의해 주세요.</p>
</div>
<!-- Bottom Navigation Actions -->
<div class="dt-actions">
@@ -306,4 +317,4 @@
</body>
</html>
</html>
@@ -1,125 +1,142 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
layout:decorate="~{layout/djbank_title_layout}">
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security" layout:decorate="~{layout/djbank_title_layout}">
<head>
<title>API 키 상세</title>
</head>
<body>
<section layout:fragment="title">
<div class="page-title-banner">
<img th:src="@{/img/img_title_bg.png}" class="title-image">
<h1>앱관리</h1>
</div>
</section>
<th:block layout:fragment="contentFragment">
<div class="signup-guide-v2 figma-register-wrapper">
<div class="service-main app-management-layout">
<section layout:fragment="title">
<div class="page-title-banner">
<img th:src="@{/img/img_title_bg.png}" class="title-image">
<h1>앱관리</h1>
</div>
</section>
<th:block layout:fragment="contentFragment">
<div class="signup-guide-v2 figma-register-wrapper">
<div class="service-main app-management-layout">
<!-- Left Sidebar (Service Sidebar) -->
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('apiKey')}"></th:block>
<!-- Left Sidebar (Service Sidebar) -->
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('apiKey')}"></th:block>
<!-- Content Area -->
<div class="app-management-content">
<div class="detail-wrap">
<!-- Title Bar -->
<div class="common-title-bar">
<h2 class="common-title">인증키 정보</h2>
<span class="common-subtitle">등록된 앱의 상세 정보를 확인할 수 있습니다.</span>
</div>
<!-- Content Area -->
<div class="app-management-content">
<div class="detail-wrap">
<!-- Title Bar -->
<div class="board-header">
<h2 class="board-title">인증키 정보</h2>
<span class="board-desc">등록된 앱의 상세 정보를 확인할 수 있습니다.</span>
</div>
<!-- App Basic Info Card -->
<div class="dt-app-card">
<div class="dt-app-icon-box">
<img th:if="${apiKey.appIconFileId != null}" th:src="@{/file/download(fileSn=1,fileId=${apiKey.appIconFileId})}" alt="앱 아이콘">
<svg th:unless="${apiKey.appIconFileId != null}" width="46" height="46" viewBox="0 0 24 24" fill="none" stroke="#64748b" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
</div>
<div class="dt-app-info">
<div class="dt-app-status-row">
<span th:classappend="${apiKey.appstatus == '1' ? 'status-approved' : 'status-inactive'}" class="dt-status-badge" th:text="${apiKey.appstatus == '1' ? '승인' : '비활성화'}">승인</span>
</div>
<h3 class="dt-app-name" th:text="${apiKey.clientname}">앱 이름</h3>
<p class="dt-app-desc" th:text="${apiKey.appDescription != null ? apiKey.appDescription : '앱 설명이 없습니다.'}">앱 설명이 표시됩니다.</p>
</div>
</div>
<!-- App Basic Info Card -->
<div class="dt-app-card">
<div class="dt-app-icon-box">
<img th:if="${apiKey.appIconFileId != null}"
th:src="@{/file/download(fileSn=1,fileId=${apiKey.appIconFileId})}" alt="앱 아이콘">
<svg th:unless="${apiKey.appIconFileId != null}" width="46" height="46" viewBox="0 0 24 24" fill="none"
stroke="#64748b" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
</div>
<div class="dt-app-info">
<div class="dt-app-status-row">
<span th:classappend="${apiKey.appstatus == '1' ? 'status-approved' : 'status-inactive'}"
class="dt-status-badge" th:text="${apiKey.appstatus == '1' ? '승인' : '비활성화'}">승인</span>
</div>
<h3 class="dt-app-name" th:text="${apiKey.clientname}">앱 이름</h3>
<p class="dt-app-desc"
th:text="${apiKey.appDescription != null ? apiKey.appDescription : '앱 설명이 없습니다.'}">앱 설명이 표시됩니다.</p>
</div>
</div>
<!-- Credential Info Section Card -->
<div class="dt-info-container">
<!-- Credential Info Section Card -->
<div class="dt-info-container">
<!-- Client ID -->
<div class="dt-row">
<div class="dt-col">
<label class="dt-label">Client ID</label>
<div class="dt-input-group">
<div class="dt-input-box" th:text="${apiKey.clientid}">K00000001</div>
<button type="button" class="dt-btn-copy" th:data-secret="${apiKey.clientid}" onclick="copyToClipboardFromButton(this)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<span>복사</span>
</button>
</div>
</div>
</div>
<!-- Client ID -->
<div class="dt-row">
<div class="dt-col">
<label class="dt-label">Client ID</label>
<div class="dt-input-group">
<div class="dt-input-box" th:text="${apiKey.clientid}">K00000001</div>
<button type="button" class="dt-btn-copy" th:data-secret="${apiKey.clientid}"
onclick="copyToClipboardFromButton(this)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<span>복사</span>
</button>
</div>
</div>
</div>
<!-- Client Secret -->
<div class="dt-row">
<div class="dt-col">
<label class="dt-label">
Client Secret
<button type="button" class="btn-guide-info" onclick="showLostKeyGuide()" title="비밀정보 안내" aria-label="비밀정보 안내"
style="margin-left:6px;width:18px;height:18px;border-radius:50%;border:1px solid #bbb;background:#f5f5f5;color:#666;font-size:12px;line-height:1;cursor:pointer;padding:0;vertical-align:middle;">?</button>
</label>
<!-- Client Secret -->
<div class="dt-row">
<div class="dt-col">
<label class="dt-label">
Client Secret
<button type="button" class="btn-guide-info" onclick="showLostKeyGuide()" title="비밀정보 안내"
aria-label="비밀정보 안내"
style="margin-left:6px;width:18px;height:18px;border-radius:50%;border:1px solid #bbb;background:#f5f5f5;color:#666;font-size:12px;line-height:1;cursor:pointer;padding:0;vertical-align:middle;">?</button>
</label>
<!-- secret이 아직 DB에 존재: 최초 1회 조회 가능 -->
<th:block th:if="${secretAvailable}">
<!-- View Button (shown initially) -->
<div class="dt-input-group" id="hiddenSecretBox">
<button type="button" style="width: 100%; border-radius: 10px; background-color: #2a69de; color: #fff; height: 47px; display: flex; justify-content: center; align-items: center; border: none; font-size: 16px; font-weight: 500; cursor: pointer; transition: background-color 0.2s;" onmouseover="this.style.backgroundColor='#1d56c0'" onmouseout="this.style.backgroundColor='#2a69de'" onclick="showPasswordPrompt()">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 8px;">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
<span>조회</span>
</button>
</div>
<!-- secret이 아직 DB에 존재: 최초 1회 조회 가능 -->
<th:block th:if="${secretAvailable}">
<!-- View Button (shown initially) -->
<div class="dt-input-group" id="hiddenSecretBox">
<button type="button"
style="width: 100%; border-radius: 10px; background-color: #2a69de; color: #fff; height: 47px; display: flex; justify-content: center; align-items: center; border: none; font-size: 16px; font-weight: 500; cursor: pointer; transition: background-color 0.2s;"
onmouseover="this.style.backgroundColor='#1d56c0'"
onmouseout="this.style.backgroundColor='#2a69de'" onclick="showPasswordPrompt()">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
style="margin-right: 8px;">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
<span>조회</span>
</button>
</div>
<!-- Actual Secret (hidden initially, 값은 서버 응답으로 JS가 주입) -->
<div id="revealedSecretBox" style="display: none; width: 100%;">
<div class="dt-input-group">
<div class="dt-input-box" id="revealedSecretValue" style="flex: 1;"></div>
<button type="button" class="dt-btn-copy" id="revealedSecretCopyBtn" onclick="copyToClipboardFromButton(this)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<span>복사</span>
</button>
</div>
<p class="secret-warning" style="margin:10px 0 0;padding:10px 12px;background:#fdecea;border:1px solid #f5c6c2;border-radius:6px;color:#c0392b;font-size:13px;line-height:1.6;">
⚠ 이 값은 <strong>지금 한 번만</strong> 표시되며, 이후에는 다시 조회할 수 없습니다.<br>
반드시 <strong>안전한 곳에 복사하여 보관</strong>해 주세요.
</p>
</div>
</th:block>
<!-- Actual Secret (hidden initially, 값은 서버 응답으로 JS가 주입) -->
<div id="revealedSecretBox" style="display: none; width: 100%;">
<div class="dt-input-group">
<div class="dt-input-box" id="revealedSecretValue" style="flex: 1;"></div>
<button type="button" class="dt-btn-copy" id="revealedSecretCopyBtn"
onclick="copyToClipboardFromButton(this)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<span>복사</span>
</button>
</div>
<p class="secret-warning"
style="margin:10px 0 0;padding:10px 12px;background:#fdecea;border:1px solid #f5c6c2;border-radius:6px;color:#c0392b;font-size:13px;line-height:1.6;">
⚠ 이 값은 <strong>지금 한 번만</strong> 표시되며, 이후에는 다시 조회할 수 없습니다.<br>
반드시 <strong>안전한 곳에 복사하여 보관</strong>해 주세요.
</p>
</div>
</th:block>
<!-- secret이 이미 노출·삭제됨 -->
<div th:unless="${secretAvailable}" class="secret-revealed-notice" style="display:flex;align-items:center;gap:12px;">
<span class="secret-revealed-text" style="color:#888;font-size:14px;">이미 1회 노출되어 삭제된 인증정보입니다.</span>
<button type="button" class="btn-guide-link" onclick="showLostKeyGuide()"
style="background:none;border:none;color:#2b6cb0;text-decoration:underline;cursor:pointer;font-size:13px;padding:0;">분실 시 안내</button>
</div>
</div>
</div>
<!-- secret이 이미 노출·삭제됨 -->
<div th:unless="${secretAvailable}" class="secret-revealed-notice"
style="display:flex;align-items:center;gap:12px;">
<span class="secret-revealed-text" style="color:#888;font-size:14px;">이미 1회 노출되어 삭제된 인증정보입니다.</span>
<button type="button" class="btn-guide-link" onclick="showLostKeyGuide()"
style="background:none;border:none;color:#2b6cb0;text-decoration:underline;cursor:pointer;font-size:13px;padding:0;">분실
시 안내</button>
</div>
</div>
</div>
<!-- Callback URL (기능본에서 비활성 처리됨 — 렌더링되지 않음) -->
<!--/*
<!-- Callback URL (기능본에서 비활성 처리됨 — 렌더링되지 않음) -->
<!--/*
<div class="dt-row">
<div class="dt-col">
<label class="dt-label">Callback URL</label>
@@ -130,188 +147,193 @@
</div>
*/-->
<!-- 화이트 리스트 -->
<div class="dt-row">
<div class="dt-col">
<label class="dt-label">화이트 리스트</label>
<div class="dt-input-group">
<div class="dt-input-box" th:text="${apiKey.allowedips != null ? apiKey.allowedips : '설정되지 않음'}">설정되지 않음</div>
</div>
</div>
</div>
<!-- 화이트 리스트 -->
<div class="dt-row">
<div class="dt-col">
<label class="dt-label">화이트 리스트</label>
<div class="dt-input-group">
<div class="dt-input-box" th:text="${apiKey.allowedips != null ? apiKey.allowedips : '설정되지 않음'}">
설정되지 않음</div>
</div>
</div>
</div>
<!-- API 목록 -->
<div class="dt-row">
<div class="dt-col">
<label class="dt-label">API 목록</label>
<div class="dt-api-list-box">
<div class="dt-api-item" th:each="api : ${apiKey.apiList}">
<span class="dt-api-name" th:text="${api.apiDesc}">추가된 API 1</span>
<span class="dt-api-status-badge">승인</span>
</div>
<div class="dt-empty-message" th:if="${apiKey.apiList == null or apiKey.apiList.isEmpty()}">
등록된 API가 없습니다.
</div>
</div>
</div>
</div>
<!-- API 목록 -->
<div class="dt-row">
<div class="dt-col">
<label class="dt-label">API 목록</label>
<div class="dt-api-list-box">
<div class="dt-api-item" th:each="api : ${apiKey.apiList}">
<span class="dt-api-name" th:text="${api.apiDesc}">추가된 API 1</span>
<span class="dt-api-status-badge">승인</span>
</div>
<div class="dt-empty-message" th:if="${apiKey.apiList == null or apiKey.apiList.isEmpty()}">
등록된 API가 없습니다.
</div>
</div>
</div>
</div>
<!-- 최종 수정일자 -->
<div class="dt-row">
<div class="dt-col">
<label class="dt-label">최종 수정일자</label>
<div class="dt-input-group">
<div class="dt-input-box" th:text="${#temporals.format(apiKey.modifiedon, 'yyyy.MM.dd')}">2025.11.22</div>
</div>
</div>
</div>
</div>
<!-- 최종 수정일자 -->
<div class="dt-row">
<div class="dt-col">
<label class="dt-label">최종 수정일자</label>
<div class="dt-input-group">
<div class="dt-input-box" th:text="${#temporals.format(apiKey.modifiedon, 'yyyy.MM.dd')}">2025.11.22
</div>
</div>
</div>
</div>
</div>
<!-- Bottom Navigation Actions -->
<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>
<a sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" class="dt-btn-blue" th:href="@{/myapikey/modify/step1(clientId=${apiKey.clientid})}">수정</a>
</div>
<!-- Bottom Navigation Actions -->
<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>
<a sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" class="dt-btn-blue"
th:href="@{/myapikey/modify/step1(clientId=${apiKey.clientid})}">수정</a>
</div>
</div><!-- /detail-wrap -->
</div><!-- /app-management-content -->
</div><!-- /service-main -->
</div><!-- /signup-guide-v2 -->
</div><!-- /detail-wrap -->
</div><!-- /app-management-content -->
</div><!-- /service-main -->
</div><!-- /signup-guide-v2 -->
</th:block>
</th:block>
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
/*<![CDATA[*/
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
/*<![CDATA[*/
var CREDENTIAL_CLIENT_ID = /*[[${apiKey.clientid}]]*/ '';
var CREDENTIAL_CLIENT_ID = /*[[${apiKey.clientid}]]*/ '';
// 비밀정보 안내(분실 시 조치) 가이드 팝업
function showLostKeyGuide() {
customPopups.showAlert(
'Client Secret은 보안을 위해 <strong>최초 1회만 조회</strong>할 수 있으며,<br>' +
'조회하는 즉시 개발자포탈에서 <strong>영구 삭제</strong>됩니다.<br><br>' +
'값을 분실하셨다면 복구가 불가능하므로,<br>' +
'<strong>인증키를 새로 신청</strong>하여 발급받아 주세요.'
);
}
// 비밀정보 안내(분실 시 조치) 가이드 팝업
function showLostKeyGuide() {
customPopups.showAlert(
'Client Secret은 보안을 위해 <strong>최초 1회만 조회</strong>할 수 있으며,<br>' +
'조회하는 즉시 개발자포탈에서 <strong>영구 삭제</strong>됩니다.<br><br>' +
'값을 분실하셨다면 복구가 불가능하므로,<br>' +
'<strong>인증키를 새로 신청</strong>하여 발급받아 주세요.'
);
}
// Show password prompt for viewing client secret (최초 1회 노출 + 서버측 물리 삭제)
function showPasswordPrompt() {
customPopups.showPasswordInput({
title: 'Client Secret 조회',
message: '보안을 위해 비밀번호를 입력해주세요.<br>조회 즉시 값은 영구 삭제됩니다.',
onConfirm: function (password) {
$.ajax({
url: /*[[@{/myapikey/credential/reveal-secret}]]*/ '/myapikey/credential/reveal-secret',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ clientId: CREDENTIAL_CLIENT_ID, password: password }),
headers: {
'X-XSRF-TOKEN': /*[[${_csrf.token}]]*/ 'token'
}
}).done(function (response) {
if (response.success) {
customPopups.hidePasswordInput();
// 서버가 반환한 secret을 화면에 1회 주입
$('#revealedSecretValue').text(response.secret);
$('#revealedSecretCopyBtn').attr('data-secret', response.secret).data('secret', response.secret);
$('#hiddenSecretBox').hide();
$('#revealedSecretBox').fadeIn(300);
} else if (response.alreadyRevealed) {
customPopups.hidePasswordInput();
showLostKeyGuide();
} else {
customPopups.showPasswordError(response.message || '비밀번호가 일치하지 않습니다.');
}
}).fail(function () {
customPopups.showPasswordError('오류가 발생했습니다. 다시 시도해주세요.');
});
},
onCancel: function () {
// User cancelled - do nothing
}
});
}
// Copy to clipboard function - called from button with data-secret attribute
function copyToClipboardFromButton(button) {
var text = $(button).data('secret');
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function () {
alert('클립보드에 복사되었습니다.');
}).catch(function (err) {
console.error('Failed to copy:', err);
fallbackCopy(text);
});
} else {
fallbackCopy(text);
}
}
function fallbackCopy(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
alert('클립보드에 복사되었습니다.');
} catch (err) {
alert('복사에 실패했습니다.');
}
document.body.removeChild(textArea);
}
// Delete API Key function - called from button with data-client-id attribute
function deleteApiKeyFromButton(button) {
var clientId = $(button).data('client-id');
customPopups.showConfirm('정말로 이 인증키를 삭제하시겠습니까?', function (confirmed) {
if (!confirmed) {
return;
}
$('.loading-overlay').show();
const requestData = {
clientId: clientId
};
// Show password prompt for viewing client secret (최초 1회 노출 + 서버측 물리 삭제)
function showPasswordPrompt() {
customPopups.showPasswordInput({
title: 'Client Secret 조회',
message: '보안을 위해 비밀번호를 입력해주세요.<br>조회 즉시 값은 영구 삭제됩니다.',
onConfirm: function(password) {
$.ajax({
url: /*[[@{/myapikey/credential/reveal-secret}]]*/ '/myapikey/credential/reveal-secret',
url: '/myapikey/api_key_delete',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ clientId: CREDENTIAL_CLIENT_ID, password: password }),
data: JSON.stringify(requestData),
headers: {
'X-XSRF-TOKEN': /*[[${_csrf.token}]]*/ 'token'
}
}).done(function(response) {
if (response.success) {
customPopups.hidePasswordInput();
// 서버가 반환한 secret을 화면에 1회 주입
$('#revealedSecretValue').text(response.secret);
$('#revealedSecretCopyBtn').attr('data-secret', response.secret).data('secret', response.secret);
$('#hiddenSecretBox').hide();
$('#revealedSecretBox').fadeIn(300);
} else if (response.alreadyRevealed) {
customPopups.hidePasswordInput();
showLostKeyGuide();
} else {
customPopups.showPasswordError(response.message || '비밀번호가 일치하지 않습니다.');
}).done(function (response) {
if (response && response.success === false) {
customPopups.showAlert(response.msg || 'API 삭제에 실패했습니다.');
return;
}
}).fail(function() {
customPopups.showPasswordError('오류가 발생했습니다. 다시 시도해주세요.');
customPopups.showAlert(response.msg || 'API Key가 삭제되었습니다.', function () {
window.location.href = /*[[@{/myapikey}]]*/ '/myapikey';
});
}).fail(function (jqXHR, textStatus, errorThrown) {
customPopups.showAlert('API 삭제 요청 중 오류가 발생했습니다: ' + errorThrown);
}).always(function () {
$('.loading-overlay').hide();
});
},
onCancel: function() {
// User cancelled - do nothing
}
});
}
// Copy to clipboard function - called from button with data-secret attribute
function copyToClipboardFromButton(button) {
var text = $(button).data('secret');
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function() {
alert('클립보드에 복사되었습니다.');
}).catch(function(err) {
console.error('Failed to copy:', err);
fallbackCopy(text);
});
} else {
fallbackCopy(text);
}
}
function fallbackCopy(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
alert('클립보드에 복사되었습니다.');
} catch (err) {
alert('복사에 실패했습니다.');
}
document.body.removeChild(textArea);
}
/*]]>*/
</script>
// Delete API Key function - called from button with data-client-id attribute
function deleteApiKeyFromButton(button) {
var clientId = $(button).data('client-id');
customPopups.showConfirm('정말로 이 인증키를 삭제하시겠습니까?', function(confirmed) {
if (!confirmed) {
return;
}
$('.loading-overlay').show();
const requestData = {
clientId: clientId
};
$.ajax({
url: '/myapikey/api_key_delete',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(requestData),
headers: {
'X-XSRF-TOKEN': /*[[${_csrf.token}]]*/ 'token'
}
}).done(function(response) {
if (response && response.success === false) {
customPopups.showAlert(response.msg || 'API 삭제에 실패했습니다.');
return;
}
customPopups.showAlert(response.msg || 'API Key가 삭제되었습니다.', function() {
window.location.href = /*[[@{/myapikey}]]*/ '/myapikey';
});
}).fail(function(jqXHR, textStatus, errorThrown) {
customPopups.showAlert('API 삭제 요청 중 오류가 발생했습니다: ' + errorThrown);
}).always(function() {
$('.loading-overlay').hide();
});
});
}
/*]]>*/
</script>
</th:block>
</th:block>
</body>
</html>
</html>
@@ -9,8 +9,8 @@
</div>
</section>
<section layout:fragment="contentFragment">
<div class="service-main app-management-layout">
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('apiKey')}"></th:block>
<div class="service-main container" style="padding-top: 70px; padding-bottom:100px;">
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('profile')}"></th:block>
<div class="app-management-content">
<div class="corp-manager-wrapper">
<h2 class="page-outer-title">법인 기본 정보</h2>
File diff suppressed because it is too large Load Diff