5 Commits

52 changed files with 8378 additions and 4186 deletions
+5
View File
@@ -108,3 +108,8 @@ diff
*rinjae* *rinjae*
*gf63* *gf63*
# Local publishing and config files
/publishing/
/src/main/resources/application-local.yml
/src/main/resources/logback-local.xml
+2 -1
View File
@@ -254,7 +254,8 @@ CREATE TABLE DVPOWN.PT_MESSAGE_RECIPIENT
) )
; ;
create table DVPOWN.PT_TOKEN create table PT_TOKEN
(
( (
TOKEN VARCHAR2(255) not null TOKEN VARCHAR2(255) not null
primary key, primary key,
@@ -22,6 +22,10 @@ public class PortalApplication extends SpringBootServletInitializer {
private static final Logger portal_logger = LoggerFactory.getLogger(PortalApplication.class); private static final Logger portal_logger = LoggerFactory.getLogger(PortalApplication.class);
public PortalApplication() {
super();
}
public static void main(String[] args) { public static void main(String[] args) {
portal_logger.info("##### PortalApplication Start #####"); portal_logger.info("##### PortalApplication Start #####");
@@ -85,9 +85,9 @@ public class BaseDatasourceConfiguration {
persistenceUnit = "gateway"; persistenceUnit = "gateway";
} }
// 개발 환경용 // 개발 환경용 - local 프로파일에서는 스키마 검증(ddl-auto) 해제
if (env.matchesProfiles("local")) { if (env.matchesProfiles("local")) {
properties.put("hibernate.hbm2ddl.auto", "validate"); properties.put("hibernate.hbm2ddl.auto", "none");
} }
+2
View File
@@ -24,6 +24,8 @@ spring:
default-page-size: '10' default-page-size: '10'
jpa: jpa:
open-in-view: false open-in-view: false
hibernate:
ddl-auto: none
web: web:
resources: resources:
-3
View File
@@ -5,9 +5,6 @@
<property name="LOG_PATH" value="${profileLogPath}/${inst.Name:-devSvr00}"/> <property name="LOG_PATH" value="${profileLogPath}/${inst.Name:-devSvr00}"/>
<!-- <springProfile name="gf63">-->
<!-- <property name="LOG_PATH" value="d:/kjb-logs/${inst.Name:-devSvr00}"/>-->
<!-- </springProfile>-->
<!-- <property name="CONSOLE_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %magenta([%thread]) %highlight([%-3level]) %logger{5} - %msg %n" />--> <!-- <property name="CONSOLE_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %magenta([%thread]) %highlight([%-3level]) %logger{5} - %msg %n" />-->
<property name="CONSOLE_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %magenta([%thread]) %highlight([%-3level]) %logger - %msg %n" /> <property name="CONSOLE_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %magenta([%thread]) %highlight([%-3level]) %logger - %msg %n" />
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
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 380 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 380 KiB

@@ -1,3 +1,4 @@
<svg width="1920" height="314" viewBox="0 0 1920 314" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="1920" height="318" viewBox="0 0 1920 318" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1920 314H0V0.447266C158.076 115.873 527.685 196.879 958.538 196.999H960.462C1391.87 196.878 1761.87 115.662 1919.61 0H1920V314Z" fill="#CFF2FF"/> <path d="M1920 318H0V64.4473C158.076 179.873 527.685 260.879 958.538 260.999H960.462C1391.87 260.878 1761.87 179.662 1919.61 64H1920V318Z" fill="#D3EFFB"/>
<path d="M1920 281H0V13.71C158.194 148.461 528.235 243 959.499 243C1391.68 243 1762.37 148.059 1920 12.8525V0H1920V281ZM0 0V13.71H-0.000976562V0H0Z" fill="#CFF2FF" fill-opacity="0.5"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 269 B

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 174 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 133 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 159 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 149 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 239 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 537 KiB

@@ -79,6 +79,9 @@
li.innerHTML = '' li.innerHTML = ''
+ '<div class="djb-comment-meta">' + '<div class="djb-comment-meta">'
+ ' <div class="djb-comment-meta-left">' + ' <div class="djb-comment-meta-left">'
+ ' <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 2px; opacity: 0.5;">'
+ ' <path d="M5 3v5a2 2 0 0 0 2 2h5 M9 7l3 3-3 3" stroke="#000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>'
+ ' </svg>'
+ ' <span class="djb-comment-writer">' + escapeHtml(c.writerName || '(알 수 없음)') + '</span>' + ' <span class="djb-comment-writer">' + escapeHtml(c.writerName || '(알 수 없음)') + '</span>'
+ (c.adminYn === 'Y' ? ' <span class="djb-comment-admin-badge">관리자</span>' : '') + (c.adminYn === 'Y' ? ' <span class="djb-comment-admin-badge">관리자</span>' : '')
+ ' </div>' + ' </div>'
@@ -5,6 +5,7 @@
// Color Palette - Vibrant and Modern // Color Palette - Vibrant and Modern
// Primary colors // Primary colors
$primary-color: #0049B4;
$primary-blue: #0049b4; // Vibrant blue $primary-blue: #0049b4; // Vibrant blue
$secondary-blue: #c3dfea; // Deep blue $secondary-blue: #c3dfea; // Deep blue
$accent-cyan: #00D4FF; // Sky blue accent $accent-cyan: #00D4FF; // Sky blue accent
@@ -8,9 +8,12 @@
// Float animation // Float animation
@keyframes float { @keyframes float {
0%, 100% {
0%,
100% {
transform: translateY(0) rotate(0deg); transform: translateY(0) rotate(0deg);
} }
50% { 50% {
transform: translateY(-30px) rotate(10deg); transform: translateY(-30px) rotate(10deg);
} }
@@ -18,9 +21,12 @@
// Bounce animation // Bounce animation
@keyframes bounce { @keyframes bounce {
0%, 100% {
0%,
100% {
transform: translateY(0); transform: translateY(0);
} }
50% { 50% {
transform: translateY(-10px); transform: translateY(-10px);
} }
@@ -32,6 +38,7 @@
opacity: 0; opacity: 0;
transform: translateY(-30px); transform: translateY(-30px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
@@ -44,6 +51,7 @@
opacity: 0; opacity: 0;
transform: translateY(30px); transform: translateY(30px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
@@ -56,6 +64,7 @@
opacity: 0; opacity: 0;
transform: translateX(-30px); transform: translateX(-30px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateX(0); transform: translateX(0);
@@ -68,6 +77,7 @@
opacity: 0; opacity: 0;
transform: translateX(30px); transform: translateX(30px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateX(0); transform: translateX(0);
@@ -79,6 +89,7 @@
from { from {
opacity: 0; opacity: 0;
} }
to { to {
opacity: 1; opacity: 1;
} }
@@ -90,6 +101,7 @@
opacity: 0; opacity: 0;
transform: scale(0.9); transform: scale(0.9);
} }
to { to {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
@@ -101,6 +113,7 @@
from { from {
transform: rotate(0deg); transform: rotate(0deg);
} }
to { to {
transform: rotate(360deg); transform: rotate(360deg);
} }
@@ -111,9 +124,11 @@
0% { 0% {
transform: scale(1); transform: scale(1);
} }
50% { 50% {
transform: scale(1.05); transform: scale(1.05);
} }
100% { 100% {
transform: scale(1); transform: scale(1);
} }
@@ -124,6 +139,7 @@
0% { 0% {
transform: translateX(-100%) rotate(45deg); transform: translateX(-100%) rotate(45deg);
} }
100% { 100% {
transform: translateX(100%) rotate(45deg); transform: translateX(100%) rotate(45deg);
} }
@@ -134,24 +150,31 @@
0% { 0% {
transform: rotate(0deg); transform: rotate(0deg);
} }
10% { 10% {
transform: rotate(14deg); transform: rotate(14deg);
} }
20% { 20% {
transform: rotate(-8deg); transform: rotate(-8deg);
} }
30% { 30% {
transform: rotate(14deg); transform: rotate(14deg);
} }
40% { 40% {
transform: rotate(-4deg); transform: rotate(-4deg);
} }
50% { 50% {
transform: rotate(10deg); transform: rotate(10deg);
} }
60% { 60% {
transform: rotate(0deg); transform: rotate(0deg);
} }
100% { 100% {
transform: rotate(0deg); transform: rotate(0deg);
} }
@@ -163,9 +186,11 @@
opacity: 0; opacity: 0;
transform: scale(0.5); transform: scale(0.5);
} }
80% { 80% {
transform: scale(1.1); transform: scale(1.1);
} }
100% { 100% {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
@@ -178,6 +203,7 @@
opacity: 0; opacity: 0;
transform: scale(0.95); transform: scale(0.95);
} }
to { to {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
@@ -190,6 +216,7 @@
transform: scale(0); transform: scale(0);
opacity: 1; opacity: 1;
} }
100% { 100% {
transform: scale(4); transform: scale(4);
opacity: 0; opacity: 0;
@@ -201,9 +228,11 @@
0% { 0% {
background-position: 0% 50%; background-position: 0% 50%;
} }
50% { 50% {
background-position: 100% 50%; background-position: 100% 50%;
} }
100% { 100% {
background-position: 0% 50%; background-position: 0% 50%;
} }
@@ -214,6 +243,27 @@
animation: float 20s ease-in-out infinite; animation: float 20s ease-in-out infinite;
} }
@keyframes float {
0%,
100% {
transform: translateY(0px);
}
50% {
transform: translateY(-15px);
}
}
.floating-slow {
animation: float 6s ease-in-out infinite;
}
.floating-fast {
animation: float 4s ease-in-out infinite;
}
.animate-bounce { .animate-bounce {
animation: bounce 2s infinite; animation: bounce 2s infinite;
} }
@@ -222,6 +272,37 @@
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
} }
@keyframes drawLine {
to {
stroke-dashoffset: 0;
}
}
.draw-line {
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
animation: drawLine 3s ease-out forwards;
}
.pulsing {
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
transform: scale(1);
opacity: 0.9;
}
50% {
transform: scale(1.1);
opacity: 0.5;
}
}
.animate-pulse { .animate-pulse {
animation: pulse 2s ease-in-out infinite; animation: pulse 2s ease-in-out infinite;
} }
@@ -9,28 +9,30 @@
// FAQ Accordion - Figma Design // FAQ Accordion - Figma Design
.faq-accordion { .faq-accordion {
background: #F6F9FB; display: flex;
border-radius: $border-radius-lg; flex-direction: column;
overflow: hidden; gap: 6px;
margin-bottom: $spacing-2xl; margin-bottom: $spacing-2xl;
@include respond-to('sm') {
background: #FFFFFF;
border-radius: 0;
}
.faq-item { .faq-item {
border-bottom: 1px solid #DADADA; position: relative;
border-radius: 10px;
background: #fff;
transition: $transition-base; transition: $transition-base;
&:last-child {
border-bottom: none;
}
// Active state - when accordion is open // Active state - when accordion is open
&.active { &.active {
.faq-question { .faq-question {
background: #4685ef;
border-color: #e8dddd;
.faq-question-q,
.faq-question-text {
color: #fff !important;
}
.faq-icon { .faq-icon {
color: #fff;
transform: rotate(180deg); transform: rotate(180deg);
} }
} }
@@ -42,33 +44,48 @@
} }
} }
// Question row - Figma: 18px bold, #212529 // Question row
.faq-question { .faq-question {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; min-height: 47px;
padding: $spacing-md $spacing-lg; height: auto;
font-size: 18px; padding: 8px 35px 8px 21px;
font-weight: $font-weight-bold; background: #fff;
color: #212529; border: 1px solid #dedede;
border-radius: 10px;
cursor: pointer; cursor: pointer;
transition: $transition-base; transition: $transition-base;
gap: 95px;
min-height: 54px;
@media (max-width: $breakpoint-md) { @media (max-width: $breakpoint-md) {
padding: $spacing-md $spacing-lg; padding: 12px 16px;
font-size: 16px; min-height: 44px;
gap: $spacing-lg;
} }
&:hover { .faq-question-q {
background: rgba($primary-blue, 0.02); font-size: 20px;
font-weight: 700;
color: #555;
margin-right: 15px;
line-height: 1;
@media (max-width: $breakpoint-md) {
font-size: 16px;
margin-right: 10px;
}
} }
.faq-question-text { .faq-question-text {
flex: 1; flex: 1;
line-height: $line-height-normal; font-size: 14px;
color: #000;
line-height: normal;
word-break: keep-all;
@media (max-width: $breakpoint-md) {
font-size: 13px;
line-height: 1.4;
}
} }
// Chevron icon // Chevron icon
@@ -79,7 +96,7 @@
justify-content: center; justify-content: center;
width: 22px; width: 22px;
height: 22px; height: 22px;
color: #212529; color: #000;
transition: transform 0.3s ease; transition: transform 0.3s ease;
svg { svg {
@@ -89,49 +106,41 @@
} }
} }
// Answer section - white background // Answer section
.faq-answer { .faq-answer {
display: none; display: none;
background: #F6F9FB; padding: 19px 21px 25px 21px;
padding: $spacing-lg 20px $spacing-xl;
border-top: none;
@include respond-to('sm') {
background: #FFFFFF;
}
@media (max-width: $breakpoint-md) { @media (max-width: $breakpoint-md) {
padding: $spacing-md $spacing-lg $spacing-lg; padding: 16px;
} }
.faq-answer-content { .faq-answer-content {
font-size: 18px; font-size: 14px;
font-weight: $font-weight-regular; color: #000;
color: #515151; line-height: 1.6;
background-color: #ffffff;
padding: $spacing-lg;
line-height: $line-height-loose;
@media (max-width: $breakpoint-md) { @media (max-width: $breakpoint-md) {
font-size: 14px; font-size: 13px;
} }
// Content formatting // Content formatting
p { p {
margin-bottom: $spacing-md; margin-bottom: 8px;
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
} }
ul, ol { ul,
margin: $spacing-sm 0; ol {
padding-left: $spacing-lg; margin: 8px 0;
padding-left: 20px;
} }
li { li {
margin-bottom: $spacing-xs; margin-bottom: 4px;
} }
a { a {
@@ -225,6 +234,7 @@
opacity: 0; opacity: 0;
transform: translateY(-10px); transform: translateY(-10px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
@@ -41,9 +41,7 @@
// ── 댓글 목록 박스 (설계서 ① 영역) ───────────────────────── // ── 댓글 목록 박스 (설계서 ① 영역) ─────────────────────────
.djb-comment-list-box { .djb-comment-list-box {
padding: 24px 28px;
background-color: #FFFFFF; background-color: #FFFFFF;
border: 1px solid #E5E7EB;
border-radius: 8px; border-radius: 8px;
min-height: 120px; min-height: 120px;
margin-bottom: 28px; margin-bottom: 28px;
@@ -64,16 +62,16 @@
// ── 댓글 한 건 (설계서 작성자 + 날짜 + Contents) ─────────── // ── 댓글 한 건 (설계서 작성자 + 날짜 + Contents) ───────────
.djb-comment-item { .djb-comment-item {
padding: 18px 0; padding: 40px 0;
border-bottom: 1px dashed #E5E7EB; border-bottom: 1px solid #DFDFDF;
&:first-child { &:first-child {
padding-top: 4px; padding-top: 20px;
} }
&:last-child { &:last-child {
border-bottom: none; border-bottom: none;
padding-bottom: 4px; padding-bottom: 20px;
} }
&--admin .djb-comment-writer { &--admin .djb-comment-writer {
@@ -94,21 +92,21 @@
} }
.djb-comment-writer { .djb-comment-writer {
font-size: 18px; font-size: 14px;
font-weight: 700; font-weight: 500;
color: #111827; color: #000;
letter-spacing: -0.2px; letter-spacing: -0.2px;
} }
.djb-comment-admin-badge { .djb-comment-admin-badge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
padding: 2px 8px; padding: 3px 8px;
background-color: #DBEAFE; background-color: #ecf1ff;
color: #1D4ED8; color: #2a69de;
border-radius: 10px; border-radius: 10px;
font-size: 11px; font-size: 9px;
font-weight: 600; font-weight: 700;
} }
.djb-comment-meta-right { .djb-comment-meta-right {
@@ -123,29 +121,32 @@
} }
.djb-comment-delete-btn { .djb-comment-delete-btn {
background: none; background: #fbe7e9;
border: 1px solid #E5E7EB; border: 1px solid #f1c2d6;
border-radius: 4px; border-radius: 5px;
padding: 3px 10px; padding: 3px 6px;
font-size: 12px; font-size: 10px;
color: #6B7280; font-weight: 500;
color: #bb1026;
cursor: pointer; cursor: pointer;
transition: all 0.15s ease; transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
height: 20px;
&:hover { &:hover {
background-color: #FEE2E2; background-color: #f1c2d6; // Changed from darken(#fbe7e9, 5%) to fix warning
border-color: #FCA5A5;
color: #B91C1C;
} }
} }
.djb-comment-body { .djb-comment-body {
padding: 12px 16px; padding: 24px;
background-color: #F3F4F6; background-color: #f4f5f9;
border-radius: 6px; border-radius: 10px;
font-size: 14px; font-size: 14px;
line-height: 1.6; line-height: 20px;
color: #1F2937; color: #000;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
} }
@@ -167,31 +168,31 @@ textarea.djb-comment-input {
display: block; display: block;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
padding: 16px 18px; padding: 14px 18px;
border: 1px solid #D1D5DB; border: 1px solid #dfdfdf;
border-radius: 8px; border-radius: 10px;
background-color: #F9FAFB; background-color: #ffffff;
font-size: 14px; font-size: 14px;
line-height: 1.6; line-height: 1.6;
color: #1F2937; color: #000;
resize: vertical; resize: vertical;
min-height: 120px; min-height: 103px;
font-family: inherit; font-family: inherit;
outline: none; outline: none;
transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease; transition: border-color 0.15s ease, box-shadow 0.15s ease;
&::placeholder { &::placeholder {
color: #9CA3AF; color: #7f8a95;
} }
&:hover { &:hover {
border-color: #9CA3AF; border-color: #2a69de;
} }
&:focus { &:focus {
border-color: #1D4ED8; border-color: #2a69de;
background-color: #FFFFFF; background-color: #FFFFFF;
box-shadow: 0 0 0 3px rgba(29, 78, 216, 0.15); box-shadow: 0 0 0 3px rgba(42, 105, 222, 0.1);
} }
} }
@@ -209,28 +210,22 @@ textarea.djb-comment-input {
} }
button.djb-comment-submit { button.djb-comment-submit {
display: inline-block; display: flex;
min-width: 140px; align-items: center;
padding: 12px 28px; justify-content: center;
background-color: #1D4ED8; width: 149px;
color: #FFFFFF; height: 39px;
border: 1px solid #1D4ED8; background-color: #ffffff;
border-radius: 8px; color: #4685ef;
font-size: 15px; border: 1px solid #2a69de;
border-radius: 10px;
font-size: 14px;
font-weight: 700; font-weight: 700;
letter-spacing: -0.2px;
cursor: pointer; cursor: pointer;
appearance: none; transition: all 0.2s ease;
-webkit-appearance: none;
line-height: 1.2;
text-align: center;
box-shadow: 0 2px 6px rgba(29, 78, 216, 0.18);
transition: background-color 0.15s ease, box-shadow 0.15s ease, transform 0.05s ease;
&:hover { &:hover {
background-color: #1E40AF; background-color: #f8faff;
border-color: #1E40AF;
box-shadow: 0 3px 10px rgba(29, 78, 216, 0.28);
} }
&:active { &:active {
@@ -238,9 +233,9 @@ button.djb-comment-submit {
} }
&:disabled { &:disabled {
background-color: #94A3B8; background-color: #f4f5f9;
border-color: #94A3B8; border-color: #dfdfdf;
box-shadow: none; color: #7f8a95;
cursor: not-allowed; cursor: not-allowed;
} }
} }
@@ -13,7 +13,7 @@
label { label {
display: block; display: block;
font-size: $font-size-sm; font-size: $font-size-sm;
font-weight: $font-weight-medium; font-weight: $font-weight-semibold;
color: $text-dark; color: $text-dark;
margin-bottom: $spacing-sm; margin-bottom: $spacing-sm;
@@ -453,6 +453,7 @@ select.form-control {
.form-field-wrapper { .form-field-wrapper {
flex: 1; flex: 1;
@include respond-to('sm') { @include respond-to('sm') {
width: 100%; width: 100%;
} }
@@ -757,6 +758,7 @@ select.form-control {
} }
.btn { .btn {
// Figma 모바일 (1291-3170): 144px × 40px, border-radius 8px, 14px bold // Figma 모바일 (1291-3170): 144px × 40px, border-radius 8px, 14px bold
@include respond-to('sm') { @include respond-to('sm') {
width: 144px !important; width: 144px !important;
@@ -960,3 +962,71 @@ select.form-control {
display: flex; display: flex;
align-items: center; align-items: center;
} }
// -----------------------------------------------------------------------------
// DJB Common Form Elements (Global Reusable Styles)
// -----------------------------------------------------------------------------
.djb-label {
display: block;
font-size: 14px;
font-weight: 700;
color: #000;
margin-bottom: 12px;
.required {
color: #e53e3e;
margin-left: 4px;
}
}
.djb-input {
width: 100%;
padding: 14px 18px;
border: 1px solid #DFDFDF;
border-radius: 8px;
font-size: 14px;
color: #000;
transition: border-color 0.2s;
background-color: #fcfcfc;
&::placeholder {
color: #94A3B8;
}
&:focus {
outline: none;
border-color: #2a69de;
}
}
.djb-textarea {
width: 100%;
padding: 14px 18px;
border: 1px solid #DFDFDF;
border-radius: 8px;
font-size: 14px;
color: #000;
resize: none;
transition: border-color 0.2s;
min-height: 250px;
&::placeholder {
color: #94A3B8;
}
&:focus {
outline: none;
border-color: #2a69de;
}
}
.djb-char-counter-wrapper {
text-align: right;
margin-top: 8px;
.char-counter {
font-size: 13px;
color: #7f8a95;
}
}
@@ -18,46 +18,76 @@
&.authenticated { &.authenticated {
gap: 0; gap: 0;
} }
.login-btn{
font-size: 15px;
font-weight: 500;
color: var(--primary-color);
padding: 10px 20px;
border-radius: 8px;
transition: var(--transition-smooth);
&:hover{
background-color: var(--accent-light);
}
}
.btn-login:hover {
background-color: var(--accent-light);
}
} }
// =========================== // ===========================
// Login Button // Login Button
// =========================== // ===========================
.login-btn { .login-btn {
display: inline-flex; font-size: 15px;
align-items: center; font-weight: 500;
padding: 10px 20px; color: var(--primary-color);
color: $text-dark; padding: 10px 20px;
text-decoration: none; border-radius: 8px;
border: 2px solid $primary-blue; transition: var(--transition-smooth);
border-radius: $border-radius-full;
font-weight: $font-weight-semibold;
font-size: $font-size-sm;
transition: $transition-base;
// Box style for Figma design (node-id: 721-1489) &:hover {
&-box { background-color: var(--accent-light);
height: 32px; }
display: inline-flex; }
align-items: center;
justify-content: center;
gap: 6px; // 아이콘-텍스트 간격
padding: 6px 16px;
color: #212529; // 강조 텍스트 색상
text-decoration: none;
border: none;
border-radius: 6px;
font-family: $font-family-primary;
font-weight: $font-weight-semibold; // 600
font-size: 16px;
background: transparent;
transition: $transition-base;
.user-icon {
width: 18px; .btn-signup {
height: 18px; font-size: 15px;
flex-shrink: 0; font-weight: 700;
} color: #FFFFFF;
background-color: var(--primary-color);
padding: 10px 24px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 73, 180, 0.15);
transition: var(--transition-smooth);
&:hover {
background-color: var(--secondary-color);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 73, 180, 0.25);
color:white;
}
}
// Box style for Figma design (node-id: 721-1489)
.login-btn-box {
font-size: 15px;
font-weight: 500;
color: var(--primary-color);
padding: 10px 20px;
border-radius: 8px;
transition: var(--transition-smooth);
.user-icon {
width: 18px;
height: 18px;
flex-shrink: 0;
} }
} }
@@ -20,6 +20,7 @@ $pagination-text-disabled: #adb5bd;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: $pagination-gap; gap: $pagination-gap;
margin-top:50px;
// Page navigation buttons (first, prev, next, last) // Page navigation buttons (first, prev, next, last)
.page-first, .page-first,
@@ -275,10 +275,138 @@
} }
} }
// -----------------------------------------------------------------------------
// Board Table - Standard BBS design (Figma: 165-125)
// Used in: Notice, Q&A, FAQ etc.
// -----------------------------------------------------------------------------
.board-table {
width: 100%;
border-top: 1px solid #c4c7c8;
&-wrapper {
overflow-x: auto;
margin-bottom: $spacing-xl;
}
// Table Header - Gray background (#f9f9f9)
.board-table-header {
display: flex;
align-items: center;
background: #f9f9f9;
height: 50px;
padding: 0 45px;
border-bottom: 1px solid #dedede;
@media (max-width: $breakpoint-sm) {
display: none;
}
.header-cell {
font-size: 14px;
font-family: 'Spoqa Han Sans Neo', sans-serif;
font-weight: 500;
color: #000;
text-align: center;
}
}
// Table Body
.board-table-body {
display: flex;
flex-direction: column;
}
// Table Row
.board-table-row {
display: flex;
align-items: center;
height: 50px;
padding: 0 45px;
border-bottom: 1px solid #dedede;
transition: background-color 0.2s;
background: #ffffff;
&:hover {
background-color: #f8fbfe;
}
@media (max-width: $breakpoint-sm) {
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: $breakpoint-sm) {
justify-content: flex-start;
width: 100% !important;
&::before {
content: attr(data-label);
font-weight: 500;
color: #64748b;
margin-right: 10px;
min-width: 60px;
}
}
&--title {
justify-content: flex-start;
white-space: normal;
@media (max-width: $breakpoint-sm) {
&::before { display: none; }
}
.notice-title-link {
display: flex;
align-items: center;
gap: 10px;
color: inherit;
text-decoration: none;
width: 100%;
&:hover {
text-decoration: underline;
}
// Mobile number prefix [1] [2] etc
.notice-number {
display: none;
font-weight: 500;
color: #64748b;
flex-shrink: 0;
@media (max-width: $breakpoint-sm) {
display: inline;
}
}
}
}
&--number {
@media (max-width: $breakpoint-sm) {
display: none;
}
}
}
}
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// List Table - Modern design with colored header (Figma: 984-2173) // List Table - Modern design with colored header (Figma: 984-2173)
// Used in: User management, API key list, etc. // Used in: User management, API key list, etc.
// Refined: Clean Minimal sizing with original KJB colors
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
.list-table { .list-table {
width: 100%; width: 100%;
@@ -459,17 +587,17 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 0 $spacing-sm; padding: 0 $spacing-md;
height: 30px; height: 28px;
white-space: nowrap; white-space: nowrap;
border-radius: $border-radius-sm; border-radius: 5px;
font-size: 13px; font-size: 12px;
font-weight: $font-weight-regular; font-weight: $font-weight-regular;
font-family: 'Spoqa Han Sans Neo', sans-serif;
color: #000000; color: #000000;
border: none; border: 1px solid transparent;
cursor: pointer; cursor: pointer;
transition: $transition-base; transition: $transition-base;
white-space: nowrap;
&:hover { &:hover {
transform: translateY(-1px); transform: translateY(-1px);
@@ -482,35 +610,42 @@
// Button variants // Button variants
&--default { &--default {
background-color: #DADADA; background-color: #f5f5f4;
border-color: #bdc7cf;
color: #64748b;
&:hover { &:hover {
background-color: darken(#DADADA, 5%); background-color: darken(#f5f5f4, 5%);
} }
} }
&--primary { &--primary {
background-color: #A4D6EA; background-color: #ecf0fa;
border-color: #4685ef;
color: #2a69de;
&:hover { &:hover {
background-color: darken(#A4D6EA, 5%); background-color: darken(#ecf0fa, 5%);
} }
} }
&--secondary { &--secondary {
background-color: #CDD4F0; background-color: #f5f5f4;
border-color: #bdc7cf;
color: #64748b;
&:hover { &:hover {
background-color: darken(#CDD4F0, 5%); background-color: darken(#f5f5f4, 5%);
} }
} }
&--danger { &--danger {
background-color: #F8D7DA; background-color: #fbe7e9;
color: #B02A37; border-color: #f1c2d6;
color: #bb1026;
&:hover { &:hover {
background-color: darken(#F8D7DA, 5%); background-color: darken(#fbe7e9, 5%);
} }
} }
} }
@@ -6,6 +6,7 @@
// Footer Styles - Figma Design Implementation // Footer Styles - Figma Design Implementation
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
/* 원래 있던 Footer 스타일 (주석 처리)
.global-footer { .global-footer {
background: #FFFFFF; background: #FFFFFF;
border-top: 1px solid #E5E7EB; border-top: 1px solid #E5E7EB;
@@ -142,6 +143,143 @@
} }
} }
} }
*/
/* 피그마 시안 적용 다크 모드 Footer 스타일 */
.global-footer {
background-color: rgb(15, 23, 42);
color: rgb(100, 116, 139);
padding: 60px 0px;
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
}
.footer-content {
display: flex;
justify-content: space-between;
align-items: center;
gap: 40px;
@include respond-to('md') {
flex-direction: column;
align-items: flex-start;
gap: 32px;
padding: 32px 0;
}
}
// Left Section
.footer-left {
display: flex;
flex-direction: column;
gap: 16px;
.footer-logo {
width: 114px;
height: auto;
object-fit: contain;
}
.footer-links {
display: flex;
align-items: center;
gap: 8px;
.footer-link {
font-family: "Spoqa Han Sans Neo", "SpoqaHanSans", "Noto Sans KR", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 16px;
font-weight: 400;
color: #9CA3AF;
text-decoration: none;
transition: color 0.3s ease;
&:hover {
color: #FFFFFF;
text-decoration: underline;
}
}
.footer-separator {
color: #9CA3AF;
font-size: 16px;
user-select: none;
}
}
.footer-copyright {
font-family: $font-family-primary;
font-size: 14px;
font-weight: 400;
color: #9CA3AF;
margin: 0;
line-height: 1.5;
}
}
// Right Section
.footer-right {
display: flex;
flex-direction: column;
gap: 16px;
align-items: flex-end;
@include respond-to('md') {
align-items: flex-start;
width: 100%;
}
.footer-related-sites {
.related-sites-select {
width: 240px;
height: 44px;
padding: 0 16px;
font-family: $font-family-primary;
font-size: 16px;
font-weight: 400;
color: #9CA3AF;
background: rgb(30, 41, 59);
border: 1px solid rgb(71, 85, 105);
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L6 6L11 1' stroke='%239CA3AF' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 16px center;
padding-right: 40px;
&:hover {
border-color: #FFFFFF;
}
&:focus {
outline: none;
border-color: #FFFFFF;
}
@include respond-to('md') {
width: 100%;
}
}
}
.footer-contact {
font-family: "Spoqa Han Sans Neo", "SpoqaHanSans", "Noto Sans KR", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 18px;
font-weight: 700;
color: #9CA3AF;
margin: 0;
line-height: 1.2;
@include respond-to('md') {
font-size: 16px;
}
}
}
// Mobile responsive adjustments (sm: 768px) // Mobile responsive adjustments (sm: 768px)
@include respond-to('sm') { @include respond-to('sm') {
@@ -9,6 +9,8 @@
// CSS Variables for modern header styling // CSS Variables for modern header styling
:root { :root {
// Enhanced color palette from design // Enhanced color palette from design
--primary-color: #0049b4;
--secondary-color: #008ae2;
--primary-blue: #4B9BFF; --primary-blue: #4B9BFF;
--secondary-blue: #2E7FF7; --secondary-blue: #2E7FF7;
--accent-cyan: #00D4FF; --accent-cyan: #00D4FF;
@@ -16,6 +18,8 @@
--accent-orange: #FF6B6B; --accent-orange: #FF6B6B;
--accent-green: #6BCF7F; --accent-green: #6BCF7F;
--accent-purple: #A78BFA; --accent-purple: #A78BFA;
--accent-color:#efdcb2;
--accent-light: #E9F9FF;
// Base colors // Base colors
--text-dark: #1A1A2E; --text-dark: #1A1A2E;
@@ -31,6 +35,13 @@
--shadow-md: 0 4px 12px rgba(75, 155, 255, 0.1); --shadow-md: 0 4px 12px rgba(75, 155, 255, 0.1);
--shadow-lg: 0 8px 24px rgba(75, 155, 255, 0.15); --shadow-lg: 0 8px 24px rgba(75, 155, 255, 0.15);
--shadow-xl: 0 16px 40px rgba(75, 155, 255, 0.2); --shadow-xl: 0 16px 40px rgba(75, 155, 255, 0.2);
--shadow-hover: 0 30px 60px -10px rgba(0, 73, 180, 0.2);
// border
--border-color : #E2E8F0;
//transition
--transition-smooth: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
} }
// =========================== // ===========================
@@ -91,16 +102,28 @@
@media (max-width: 768px) { @media (max-width: 768px) {
height: 40px; height: 40px;
.survey-label { display: none; }
.survey-btn { padding: 4px 12px; font-size: 12px; } .survey-label {
display: none;
}
.survey-btn {
padding: 4px 12px;
font-size: 12px;
}
} }
} }
// Body offset when survey is active // Body offset when survey is active
body.design-survey-active { body.design-survey-active {
.global-header { margin-top: 48px; } .global-header {
margin-top: 48px;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.global-header { margin-top: 40px; } .global-header {
margin-top: 40px;
}
} }
} }
@@ -124,35 +147,63 @@ body.design-survey-active {
// Modern Header Structure // Modern Header Structure
// =========================== // ===========================
.global-header { .global-header {
position: relative; position: fixed;
background-color: #ffffff; top: 0;
z-index: $z-index-header; left: 0;
height: 112px; width: 100%;
transition: all 0.3s ease; height: 80px;
border-bottom: 1px solid #dddddd; background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(15px);
border-bottom: 1px solid rgba(0, 73, 180, 0.08);
z-index: 1000;
display: flex;
align-items: center;
transition: var(--transition-smooth);
// Index page header with light blue background // Index page header with light blue background
&.index-header { &.index-header {
background-color: #E5F8FF; background-color: white;
} }
.container { .container {
height: 100%; height: 100%;
padding: 0; padding: 0;
width: 100%;
.header-content { .header-content {
height: 100%; height: 100%;
} }
} }
@media (max-width: 768px) {
height: clamp(44px, 11.73vw, 60px);
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-bottom: none;
.container {
padding: 0;
max-width: 100%;
background-color: #FEFEFE;
.header-content {
padding: 0 clamp(16px, 4.27vw, 24px) 0 clamp(20px, 5.33vw, 28px);
height: clamp(44px, 11.73vw, 60px);
margin-top: 0;
}
}
}
} }
.header-content { .header-content {
width: 100%;
max-width: 1280px;
margin: 0 auto;
padding: 0 24px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
height: 65px;
max-width: 1920px;
margin: 0 auto;
padding: 0 20px;
} }
// =========================== // ===========================
@@ -166,7 +217,6 @@ body.design-survey-active {
.logo { .logo {
height: 32px; height: 32px;
width: 114px;
} }
} }
@@ -175,13 +225,27 @@ body.design-survey-active {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
font-size:24px;
.logo-text-bold {
font-weight: 700;
color: var(--primary-color);
letter-spacing: -0.5px;
}
.logo-text-thin {
font-weight: 300;
color: var(--secondary-color);
margin-left: 4px;
}
} }
// Logo with modern styling // Logo with modern styling
.logo { .logo {
display: block; display: block;
height: 32px; height: 32px;
width: 114px;
vertical-align: middle; vertical-align: middle;
z-index: 10; z-index: 10;
@@ -381,9 +445,11 @@ body.design-survey-active {
&:nth-child(1) { &:nth-child(1) {
transform: rotate(45deg) translateY(5px); transform: rotate(45deg) translateY(5px);
} }
&:nth-child(2) { &:nth-child(2) {
opacity: 0; opacity: 0;
} }
&:nth-child(3) { &:nth-child(3) {
transform: rotate(-45deg) translateY(-5px); transform: rotate(-45deg) translateY(-5px);
} }
@@ -801,14 +867,27 @@ body.design-survey-active {
transition: all $transition-fast; transition: all $transition-fast;
} }
&::before { top: -8px; } &::before {
&::after { top: 8px; } top: -8px;
}
&::after {
top: 8px;
}
} }
&.on span { &.on span {
background: transparent; background: transparent;
&::before { top: 0; transform: rotate(45deg); }
&::after { top: 0; transform: rotate(-45deg); } &::before {
top: 0;
transform: rotate(45deg);
}
&::after {
top: 0;
transform: rotate(-45deg);
}
} }
} }
@@ -848,7 +927,7 @@ body.design-survey-active {
.nav-item { .nav-item {
border-bottom: 1px solid $border-gray; border-bottom: 1px solid $border-gray;
> a { >a {
display: flex; display: flex;
align-items: center; align-items: center;
padding: $spacing-lg; padding: $spacing-lg;
@@ -859,7 +938,7 @@ body.design-survey-active {
position: relative; position: relative;
} }
&.has-submenu > a::after { &.has-submenu>a::after {
content: ''; content: '';
position: absolute; position: absolute;
right: $spacing-lg; right: $spacing-lg;
@@ -872,7 +951,7 @@ body.design-survey-active {
transition: transform $transition-fast; transition: transform $transition-fast;
} }
&.expanded > a::after { &.expanded>a::after {
transform: translateY(-25%) rotate(-135deg); transform: translateY(-25%) rotate(-135deg);
} }
@@ -885,31 +964,33 @@ body.design-survey-active {
a { a {
display: block; display: block;
padding: $spacing-md $spacing-lg $spacing-md ($spacing-lg * 2); padding: $spacing-md $spacing-lg $spacing-md (
color: $text-gray; $spacing-lg * 2
font-size: $font-size-sm; );
text-decoration: none; color: $text-gray;
font-size: $font-size-sm;
text-decoration: none;
&:hover { &:hover {
background: $white; background: $white;
color: $primary-blue; color: $primary-blue;
}
} }
} }
}
&.expanded .sub-menu { &.expanded .sub-menu {
display: block; display: block;
}
} }
} }
}
.info_wrap { .info_wrap {
padding: $spacing-xl $spacing-lg; padding: $spacing-xl $spacing-lg;
background: $white; background: $white;
border-top: 1px solid $border-gray; border-top: 1px solid $border-gray;
margin-top: auto; margin-top: auto;
// styles for tit1, tit2... // styles for tit1, tit2...
} }
} }
// =========================== // ===========================
@@ -918,16 +999,16 @@ body.design-survey-active {
.nav-menu { .nav-menu {
display: flex; display: flex;
list-style: none; list-style: none;
gap: 8px; gap: 36px;
margin: 0; margin: 0;
padding: 0; padding: 0;
// Navigation item with submenu // Navigation item with submenu
> li { >li {
position: relative; position: relative;
&.submenu-open { &.submenu-open {
> .nav-link { >.nav-link {
background: var(--light-bg); background: var(--light-bg);
color: var(--primary-blue); color: var(--primary-blue);
} }
@@ -980,16 +1061,12 @@ body.design-survey-active {
} }
.nav-link { .nav-link {
display: flex; font-size: 16px;
align-items: center;
gap: 6px;
text-decoration: none;
color: var(--text-dark);
font-weight: 500; font-weight: 500;
font-size: 20px; color: var(--text-gray);
padding: 8px 16px; padding: 8px 0;
border-radius: 8px; position: relative;
transition: all 0.3s ease; transition: var(--transition-smooth);
&:hover { &:hover {
color: var(--primary-blue); color: var(--primary-blue);
@@ -1022,10 +1099,17 @@ body.design-survey-active {
// Desktop Navigation Styles // Desktop Navigation Styles
// =========================== // ===========================
@include respond-to('desktop') { @include respond-to('desktop') {
.m-only { display: none !important; } .m-only {
.pc-only { display: block !important; } display: none !important;
}
.btn-mobilemenu { display: none; } .pc-only {
display: block !important;
}
.btn-mobilemenu {
display: none;
}
.main-nav.m-nav { .main-nav.m-nav {
position: static; position: static;
@@ -1038,8 +1122,13 @@ body.design-survey-active {
height: 100%; height: 100%;
flex: 1; flex: 1;
.nav-header { display: none; } .nav-header {
.info_wrap { display: none; } display: none;
}
.info_wrap {
display: none;
}
.nav-list { .nav-list {
display: flex; display: flex;
@@ -1051,7 +1140,7 @@ body.design-survey-active {
position: relative; position: relative;
height: 100%; height: 100%;
> a { >a {
height: 100%; height: 100%;
padding: 0 24px; padding: 0 24px;
color: var(--text-dark); color: var(--text-dark);
@@ -1061,6 +1150,7 @@ body.design-survey-active {
&:hover { &:hover {
color: var(--primary-blue); color: var(--primary-blue);
&::after { &::after {
content: ''; content: '';
position: absolute; position: absolute;
@@ -1074,7 +1164,9 @@ body.design-survey-active {
} }
} }
&.has-submenu > a::after { display: none; } &.has-submenu>a::after {
display: none;
}
} }
} }
} }
@@ -8,38 +8,128 @@
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
.api-market-container { .api-market-container {
// display: flex;
// max-width: 1240px;
// margin: 0 auto;
// min-height: 100vh;
// background-color: transparent;
// position: relative;
// margin-bottom: 20px;
max-width: 1240px;
margin: 0 auto;
padding: 60px 40px;
display: flex; display: flex;
min-height: 100vh; gap: 60px;
background-color: $white; align-items: flex-start;
}
// Hero Section
.api-market-hero {
background: linear-gradient(to right, #eff9fe, #fcfeff);
border: 1px solid #e3e8f0;
padding: 60px 0;
display: flex;
justify-content: center;
width: calc(100vw - 80px);
position: relative; position: relative;
margin-bottom: 20px; left: 50%;
transform: translateX(-50%);
margin-top: 40px;
margin-bottom: 60px;
&__inner {
max-width: 1240px;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 138px;
padding: 0 37px;
}
&__illust {
.cloud-image-placeholder {
width: 331px;
height: 229px;
background: url('../img/hero_image_1.png') no-repeat center center;
background-size: contain;
}
}
&__content {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
&__badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 20px;
background: #fff;
border: 1px solid #d1e6f0;
border-radius: 38px;
font-size: 13px;
font-family: 'Spoqa Han Sans Neo', sans-serif;
font-weight: 700;
color: #1b4ab7;
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #1b4ab7;
}
}
&__title {
font-size: 36px;
font-family: 'Spoqa Han Sans Neo', sans-serif;
font-weight: 700;
color: #000;
margin: 0;
}
&__desc {
font-size: 16px;
font-family: 'Spoqa Han Sans Neo', sans-serif;
color: #64748b;
line-height: 1.6;
margin: 0;
}
} }
// Sidebar Navigation // Sidebar Navigation
.api-market-sidebar { .api-market-sidebar {
width: 283px; width: 213px;
background-color: #F8FBFD; background-color: #ffffff;
border: 1px solid #e3e8f0;
border-radius: 20px; border-radius: 20px;
padding: $spacing-2xl $spacing-lg; padding: 28px 30px;
position: sticky; position: sticky;
top: $spacing-lg; top: 40px;
height: calc(100vh - 32px); height: fit-content;
flex-shrink: 0; flex-shrink: 0;
margin: $spacing-md; margin-right: 40px;
@media (max-width: $breakpoint-md) { @media (max-width: $breakpoint-md) {
width: 240px; width: 200px;
margin-right: 20px;
} }
@media (max-width: $breakpoint-sm) { @media (max-width: $breakpoint-sm) {
position: fixed; position: fixed;
left: 0; left: 0;
top: 60px; // Below mobile header top: 60px;
transform: translateX(-100%); transform: translateX(-100%);
transition: transform 0.3s ease; transition: transform 0.3s ease;
box-shadow: $shadow-lg; box-shadow: $shadow-lg;
z-index: $z-index-modal; z-index: $z-index-modal;
height: calc(100vh - 60px); // Full height minus header height: calc(100vh - 60px);
margin: 0; margin: 0;
border-radius: 0; border-radius: 0;
@@ -49,67 +139,37 @@
} }
} }
.api-sidebar-header {
display: flex;
justify-content: center;
margin-bottom: $spacing-xl;
padding-bottom: $spacing-lg;
img {
max-width: 100%;
height: auto;
}
}
.api-sidebar-nav { .api-sidebar-nav {
display: flex;
flex-direction: column;
gap: 26px;
.menu-section { .menu-section {
margin-bottom: 0; margin-bottom: 0;
border-bottom: 1px solid #DDDDDD; border-bottom: none;
&:last-child {
border-bottom: none;
}
} }
.menu-title { .menu-title {
padding: 16px 10px; padding: 0;
font-size: $font-size-lg; font-size: 16px;
font-weight: $font-weight-regular; font-family: 'Spoqa Han Sans Neo', sans-serif;
color: $text-dark; font-weight: 500;
color: #0d0e11;
cursor: pointer; cursor: pointer;
transition: $transition-base; transition: color 0.2s;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
position: relative; position: relative;
&:hover { &:hover {
background-color: rgba($primary-blue, 0.05); color: #2a69de;
background-color: transparent;
} }
&.active { &.active {
font-weight: $font-weight-bold; font-weight: 700;
color: $primary-blue; color: #2a69de;
}
.api-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 24px;
height: 24px;
padding: 0 $spacing-sm;
background-color: rgba($primary-blue, 0.1);
color: $primary-blue;
font-size: $font-size-xs;
font-weight: $font-weight-semibold;
border-radius: $border-radius-full;
transition: $transition-base;
}
&.active .api-count {
background-color: $primary-blue;
color: $white;
} }
} }
@@ -220,9 +280,8 @@
// Main Content Area // Main Content Area
.api-market-content { .api-market-content {
flex: 1; flex: 1;
padding-top: $spacing-5xl; // padding-left: $spacing-sm;
padding-left: $spacing-sm; // padding-right: $spacing-sm;
padding-right: $spacing-sm;
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -311,7 +370,7 @@
// Search Box // Search Box
.api-market-search { .api-market-search {
position: relative; position: relative;
width: 300px; width: 228px;
@media (max-width: $breakpoint-sm) { @media (max-width: $breakpoint-sm) {
width: 100%; width: 100%;
@@ -325,53 +384,41 @@
input { input {
flex: 1; flex: 1;
padding: 12px 48px 12px $spacing-md; height: 44px;
border: 1px solid $border-gray; padding: 0 40px 0 16px;
border-radius: $border-radius-lg; border: 1px solid #dadada;
font-size: $font-size-sm; border-radius: 12px;
font-size: 14px;
outline: none; outline: none;
transition: $transition-base; transition: $transition-base;
&:focus { &:focus {
border-color: $primary-blue; border-color: #0049b4;
box-shadow: 0 0 0 3px rgba($primary-blue, 0.1); box-shadow: 0 0 0 3px rgba(#0049b4, 0.1);
} }
&::placeholder { &::placeholder {
color: $text-light; color: #7f8a95;
} }
} }
.search-submit-btn { .search-submit-btn {
position: absolute; position: absolute;
right: 4px; right: 8px;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
width: 36px; width: 20px;
height: 36px; height: 20px;
background: transparent; background: transparent;
border: none; border: none;
border-radius: $border-radius-md;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: $transition-base;
color: $text-gray;
&:hover { svg {
background: $light-bg; width: 100%;
color: $primary-blue; height: 100%;
transform: translateY(-50%) scale(1.05);
}
&:active {
transform: translateY(-50%) scale(0.95);
}
.search-icon {
font-size: 18px;
display: block;
} }
} }
} }
@@ -381,101 +428,131 @@
// Card Grid // Card Grid
.api-card-grid { .api-card-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 320px); // 3 cards per row on desktop (고정폭) grid-template-columns: repeat(3, 1fr);
gap: $spacing-lg; gap: 24px;
justify-content: center; justify-content: flex-start;
// Tablet view - 2 cards per row (고정폭) // Tablet view
@media (max-width: $breakpoint-md) { @media (max-width: 1300px) {
grid-template-columns: repeat(2, 320px); grid-template-columns: repeat(2, 312px);
} }
// Mobile view - 1 card per row (화면에 맞춤) // Mobile view
@media (max-width: $breakpoint-sm) { @media (max-width: $breakpoint-md) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
justify-content: center;
} }
} }
// API Card // API Card
.api-card { .api-card {
background: $white; background: #FFFFFF;
border-radius: $border-radius-lg; border-radius: 20px;
padding: $spacing-xl; border: 1px solid #DDDDDD;
box-shadow: $shadow-sm; box-shadow: 0px 4px 2px rgba(194, 194, 194, 0.25);
cursor: pointer; cursor: pointer;
display: flex;
flex-direction: column;
gap: $spacing-md;
min-height: 180px;
position: relative; position: relative;
border: 1px solid $border-gray; width: 100%;
height: 312px;
overflow: hidden;
transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease;
&:hover,
&:focus,
&:active { &:active {
transform: scale(0.98); transform: translateY(-5px);
} box-shadow: 0px 8px 12px rgba(194, 194, 194, 0.4);
} background: linear-gradient(to bottom, #50caff, #2088ff);
border: none;
// Group Info Section (Icon + Name) .api-card-badge {
.api-card-group { background-color: #d8f0ff;
display: flex; }
align-items: center;
gap: $spacing-sm;
}
.api-card-group-icon { .api-card-title,
width: 32px; .api-card-desc {
height: 32px; color: #ffffff;
display: flex; }
align-items: center;
justify-content: center;
background: $gray-bg;
border-radius: $border-radius-md;
flex-shrink: 0;
img {
width: 20px;
height: 20px;
object-fit: contain;
} }
i { &-badge {
font-size: 16px; position: absolute;
color: $primary-blue; top: 39px;
left: 32px;
background-color: #f2f2f3;
border-radius: 10px;
padding: 2px 20px;
display: inline-flex;
align-items: center;
justify-content: center;
span {
font-size: 14px;
font-family: 'Spoqa Han Sans Neo', sans-serif;
font-weight: 500;
color: #1b4ab7;
white-space: nowrap;
}
} }
}
.api-card-group-name { &-title {
font-size: $font-size-sm; position: absolute;
color: $text-gray; top: 86px;
font-weight: $font-weight-medium; left: 36px;
} right: 40px;
font-size: 18px;
font-family: 'Spoqa Han Sans Neo', sans-serif;
font-weight: 700;
color: #000000;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
text-align: left;
white-space: normal;
}
// API Name (Title) &-desc {
.api-card-name { position: absolute;
font-size: $font-size-lg; top: 135px;
font-weight: $font-weight-semibold; left: 36px;
color: $text-dark; right: 40px;
line-height: $line-height-tight; font-size: 14px;
margin: 0; font-family: 'Spoqa Han Sans Neo', sans-serif;
display: -webkit-box; font-weight: 400;
-webkit-line-clamp: 2; color: #64748b;
-webkit-box-orient: vertical; line-height: 1.5;
overflow: hidden; margin: 0;
text-align: left; display: -webkit-box;
} -webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
text-align: left;
white-space: normal;
}
// API Description &-image {
.api-card-description { position: absolute;
font-size: $font-size-sm; bottom: 28px;
color: $text-gray; right: 20px;
line-height: $line-height-normal; width: 93px;
margin: 0; height: 88px;
flex-grow: 1; display: flex;
display: -webkit-box; align-items: center;
-webkit-line-clamp: 3; justify-content: center;
-webkit-box-orient: vertical; pointer-events: none;
overflow: hidden;
text-align: left; img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
}
} }
// Empty State // Empty State
@@ -832,6 +909,7 @@
// Mobile Responsive Styles (sm: 768px) // Mobile Responsive Styles (sm: 768px)
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@media (max-width: $breakpoint-sm) { @media (max-width: $breakpoint-sm) {
// API Detail Tabs - Mobile // API Detail Tabs - Mobile
.api-detail-tabs { .api-detail-tabs {
gap: 0; gap: 0;
@@ -928,7 +1006,8 @@
min-width: 500px; // 테이블 최소 너비 보장 min-width: 500px; // 테이블 최소 너비 보장
margin: $spacing-sm 0; margin: $spacing-sm 0;
th, td { th,
td {
padding: $spacing-xs $spacing-sm; padding: $spacing-xs $spacing-sm;
font-size: $font-size-xs; font-size: $font-size-xs;
white-space: nowrap; white-space: nowrap;
@@ -74,7 +74,6 @@
font-weight: $font-weight-bold; font-weight: $font-weight-bold;
color: $text-dark; color: $text-dark;
margin-bottom: $spacing-lg; margin-bottom: $spacing-lg;
padding-bottom: $spacing-md;
} }
// Detail Grid - extends common // Detail Grid - extends common
File diff suppressed because it is too large Load Diff
+284 -113
View File
@@ -26,6 +26,7 @@
.inquiry-header { .inquiry-header {
margin-bottom: $spacing-3xl; margin-bottom: $spacing-3xl;
@media (max-width: $breakpoint-sm) { @media (max-width: $breakpoint-sm) {
margin-bottom: $spacing-2xl; margin-bottom: $spacing-2xl;
} }
@@ -261,13 +262,68 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: $spacing-md; gap: $spacing-md;
margin-top: $spacing-xl; margin-top: 40px;
@media (max-width: $breakpoint-sm) { @media (max-width: $breakpoint-sm) {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
} }
// List Button - Figma Design
.btn-inquiry-new {
display: flex;
align-items: center;
justify-content: center;
width: 247px;
height: 60px;
background: #2a69de;
color: #fff;
border: none;
font-size: 20px;
font-weight: 700;
border-radius: 10px;
cursor: pointer;
text-decoration: none;
transition: $transition-base;
@media (max-width: $breakpoint-md) {
width: 100%;
height: 50px;
font-size: 16px;
}
&:hover {
background: #1d52b5; // Changed from darken(#2a69de, 10%)
transform: translateY(-2px);
}
&--outline {
background: #fff;
color: #2a69de;
border: 1px solid #2a69de;
width: 120px;
height: 50px;
font-size: 16px;
&:hover {
background: #f8faff;
}
}
&--danger {
background: #fff;
color: #bb1026;
border: 1px solid #bb1026;
width: 120px;
height: 50px;
font-size: 16px;
&:hover {
background: #fdf5f6;
}
}
}
// Layout for detail page split actions // Layout for detail page split actions
.actions-left, .actions-left,
.actions-right { .actions-right {
@@ -307,143 +363,187 @@
} }
.inquiry-detail-container { .inquiry-detail-container {
min-height: 600px; padding: 0;
margin: 0 auto; background: transparent;
box-shadow: none;
min-height: auto;
margin: 0;
// Header styles
.notice-detail-header {
border-bottom: 1px solid #000;
padding-bottom: 25px;
.inquiry-header-meta {
display: flex;
gap: 6px;
font-size: 16px;
color: #767e89;
margin-bottom: 17px;
}
.notice-detail-title-wrapper {
display: flex;
align-items: flex-end;
gap: 15px;
@media (max-width: $breakpoint-md) {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
}
.notice-detail-title {
margin: 0;
font-size: 28px;
font-weight: 500;
@media (max-width: $breakpoint-md) {
font-size: 22px;
}
}
}
// Detail body
.notice-detail-body {
padding-top: 20px;
border-bottom: 1px solid #dfdfdf;
font-size: 14px;
line-height: 20px;
color: #000;
img {
max-width: 100%;
height: auto;
}
}
// Admin response
.inquiry-response-section {
padding-top: 40px;
padding-bottom: 40px;
border-bottom: 1px solid #dfdfdf;
.inquiry-response-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0px;
border: none;
.response-admin {
display: flex;
gap: 9px;
align-items: center;
.admin-label {
font-size: 14px;
font-weight: 500;
color: #000;
}
.admin-badge {
background: #ecf1ff;
color: #2a69de;
font-size: 9px;
font-weight: 700;
padding: 3px 8px;
border-radius: 10px;
}
}
.response-meta {
color: #767e89;
font-size: 14px;
}
}
.inquiry-response-body {
background: #f4f5f9;
border-radius: 10px;
padding: 24px;
font-size: 14px;
line-height: 20px;
color: #000;
}
}
} }
// Inquiry detail card
.inquiry-detail-card {
background: $white;
border-radius: $border-radius-lg;
box-shadow: $shadow-md;
overflow: hidden;
margin-bottom: $spacing-xl;
// Header section
.detail-header {
padding: $spacing-3xl $spacing-2xl $spacing-xl;
border-bottom: 2px solid $border-gray;
@media (max-width: $breakpoint-sm) { // Attachment section
padding: $spacing-2xl $spacing-lg $spacing-md; .detail-attachments {
} padding: $spacing-lg $spacing-2xl;
background: $gray-bg;
border-bottom: 1px solid $border-gray;
// Status badge header styles now in components/_badges.scss @media (max-width: $breakpoint-sm) {
padding: $spacing-md $spacing-lg;
}
.inquiry-title { .attachment-list {
font-size: $font-size-2xl; display: flex;
font-weight: $font-weight-bold; flex-direction: column;
color: $text-dark; gap: $spacing-sm;
line-height: $line-height-tight;
margin-bottom: $spacing-lg;
@media (max-width: $breakpoint-sm) { .attachment-item {
font-size: $font-size-xl;
}
}
.inquiry-meta {
display: flex; display: flex;
align-items: center; align-items: center;
gap: $spacing-lg; gap: $spacing-sm;
flex-wrap: wrap;
font-size: $font-size-sm;
color: $text-gray;
@media (max-width: $breakpoint-sm) { a {
font-size: $font-size-xs; color: $text-dark;
gap: $spacing-md; text-decoration: none;
} font-size: $font-size-sm;
transition: $transition-base;
.meta-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: $spacing-xs; gap: $spacing-xs;
i { @media (max-width: $breakpoint-sm) {
font-size: $font-size-xs;
}
&:hover {
color: $primary-blue; color: $primary-blue;
}
strong {
color: $text-dark;
font-weight: $font-weight-medium;
}
}
}
}
// Attachment section
.detail-attachments {
padding: $spacing-lg $spacing-2xl;
background: $gray-bg;
border-bottom: 1px solid $border-gray;
@media (max-width: $breakpoint-sm) {
padding: $spacing-md $spacing-lg;
}
.attachment-list {
display: flex;
flex-direction: column;
gap: $spacing-sm;
.attachment-item {
display: flex;
align-items: center;
gap: $spacing-sm;
a {
color: $text-dark;
text-decoration: none;
font-size: $font-size-sm;
transition: $transition-base;
display: flex;
align-items: center;
gap: $spacing-xs;
@media (max-width: $breakpoint-sm) {
font-size: $font-size-xs;
}
&:hover {
color: $primary-blue;
i {
transform: translateY(-2px);
}
}
i { i {
color: $primary-blue; transform: translateY(-2px);
transition: $transition-base;
} }
} }
}
}
}
// Content section i {
.detail-content { color: $primary-blue;
padding: $spacing-3xl $spacing-2xl; transition: $transition-base;
min-height: 200px; }
@media (max-width: $breakpoint-sm) {
padding: $spacing-2xl $spacing-lg;
}
.content-body {
font-size: $font-size-base;
color: $text-dark;
line-height: $line-height-loose;
white-space: pre-line;
@media (max-width: $breakpoint-sm) {
font-size: $font-size-sm;
} }
} }
} }
} }
// Content section
.detail-content {
padding: $spacing-3xl $spacing-2xl;
min-height: 200px;
@media (max-width: $breakpoint-sm) {
padding: $spacing-2xl $spacing-lg;
}
.content-body {
font-size: $font-size-base;
color: $text-dark;
line-height: $line-height-loose;
white-space: pre-line;
@media (max-width: $breakpoint-sm) {
font-size: $font-size-sm;
}
}
}
// Response section (answer from admin) // Response section (answer from admin)
.inquiry-response-card { .inquiry-response-card {
background: $white; background: $white;
@@ -692,3 +792,74 @@
// Uses the same action button styles from .inquiry-actions above // Uses the same action button styles from .inquiry-actions above
// Supports: action-btn-primary, action-btn-secondary // Supports: action-btn-primary, action-btn-secondary
} }
// -----------------------------------------------------------------------------
// New DJB Board Write Form (피드백/개선요청, Q&A 등록 등)
// -----------------------------------------------------------------------------
.djb-board-write-container {
padding: 0;
.djb-board-write-header {
margin-bottom: 30px;
padding-bottom: 20px;
.title {
font-size: 24px;
font-weight: 700;
color: #000;
margin-bottom: 8px;
}
.subtitle {
font-size: 14px;
color: #7f8a95;
}
}
.djb-board-form {
.form-group {
margin-bottom: 30px;
}
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 40px;
.btn-cancel {
width: 100px;
height: 48px;
background-color: #fff;
border: 1px solid #DFDFDF;
border-radius: 8px;
color: #515961;
font-size: 15px;
font-weight: 700;
cursor: pointer;
transition: background-color 0.2s;
display: block;
&:hover {
background-color: #f8f9fa;
}
}
.btn-submit {
width: 100px;
height: 48px;
background-color: #2a69de;
border: 1px solid #2a69de;
border-radius: 8px;
color: #fff;
font-size: 15px;
font-weight: 700;
cursor: pointer;
&:hover {
background-color: darken(#2a69de, 5%);
}
}
}
}
+160 -64
View File
@@ -71,6 +71,7 @@
margin: 0 auto; margin: 0 auto;
padding: $spacing-3xl 0; padding: $spacing-3xl 0;
min-height: 600px; min-height: 600px;
@media (max-width: $breakpoint-md) { @media (max-width: $breakpoint-md) {
padding: $spacing-2xl $spacing-md; padding: $spacing-2xl $spacing-md;
} }
@@ -78,25 +79,91 @@
// Notice Detail Header - Title and Date in one row // Notice Detail Header - Title and Date in one row
.notice-detail-header { .notice-detail-header {
display: flex; padding-bottom: 24px;
justify-content: space-between;
align-items: center;
padding-bottom: $spacing-md;
border-bottom: 1px solid #212529; border-bottom: 1px solid #212529;
margin-bottom: $spacing-xl;
@media (max-width: $breakpoint-md) {
padding-bottom: 16px;
}
}
.notice-detail-date {
font-size: 16px;
color: #767e89;
font-weight: 400;
margin: 0 0 17px 0;
@media (max-width: $breakpoint-md) {
font-size: 14px;
margin: 0 0 12px 0;
}
}
.notice-detail-title-wrapper {
display: flex;
align-items: center;
gap: 15px;
@media (max-width: $breakpoint-md) { @media (max-width: $breakpoint-md) {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: $spacing-sm; gap: 8px;
}
}
.notice-type-badge {
display: inline-flex;
align-items: center;
justify-content: center;
height: 20px;
width: 50px;
margin-right: 6px;
border-radius: 5px;
font-size: 10px;
font-weight: 500;
box-sizing: border-box;
&--incident {
border: 1px solid #f1c2d6;
background: #fbe7e9;
color: #bb1026;
}
&--maintenance {
background: #fdf4d4;
color: #ef9546;
border: 1px solid #ffcea1;
}
&--completed {
background: #e5f0ff;
color: #0a4ea3;
border: 1px solid #b3d4ff;
}
&--closed {
background: #f5f5f5;
color: #888888;
border: 1px solid #dedede;
}
&--pending {
background: #fdf4d4;
color: #ef9546;
border: 1px solid #ffcea1;
}
&--fix {
background: #e5f0ff;
color: #0a4ea3;
border: 1px solid #b3d4ff;
} }
} }
.notice-detail-title { .notice-detail-title {
font-size: 24px; font-size: 28px;
font-weight: $font-weight-bold; font-weight: 500;
color: #212529; color: #000;
line-height: 1;
margin: 0; margin: 0;
@media (max-width: $breakpoint-md) { @media (max-width: $breakpoint-md) {
@@ -104,24 +171,20 @@
} }
} }
.notice-detail-date {
font-size: 24px;
font-weight: $font-weight-regular;
color: #515151;
@media (max-width: $breakpoint-md) {
font-size: 16px;
}
}
// Notice Attachment Section // Notice Attachment Section
.notice-detail-attachment { .notice-detail-attachment {
margin-bottom: $spacing-lg; margin-top: 20px;
margin-bottom: 20px;
padding: 15px 20px;
background: #f8fbfe;
border-radius: 8px;
display: flex;
align-items: center;
.attachment-list { .attachment-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: $spacing-sm; gap: 8px;
} }
.attachment-item { .attachment-item {
@@ -131,25 +194,20 @@
} }
.notice-attachment-link { .notice-attachment-link {
display: inline-flex; display: flex;
align-items: center; align-items: center;
gap: $spacing-xs; gap: 8px;
color: #515151;
font-size: 18px;
text-decoration: none; text-decoration: none;
color: #333;
transition: $transition-base; transition: $transition-base;
@media (max-width: $breakpoint-md) {
font-size: 14px;
}
&:hover { &:hover {
color: $primary-blue; color: $primary-blue;
} }
svg { svg {
flex-shrink: 0; flex-shrink: 0;
color: #515151; color: #333;
} }
&:hover svg { &:hover svg {
@@ -157,32 +215,68 @@
} }
.attachment-label { .attachment-label {
margin-right: $spacing-md; font-weight: 500;
font-size: 14px;
} }
.attachment-filename { .attachment-filename {
color: #515151; font-size: 14px;
color: #666;
}
}
// Incident Info Table
.notice-detail-incident-info {
margin-bottom: 20px;
.detail-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
border-top: none;
border-bottom: 1px solid #e3e8f0;
tr {
border-bottom: 1px solid #e3e8f0;
}
th {
background: #f9f9f9;
height: 43px;
font-weight: 500;
font-size: 14px;
text-align: center;
color: #000;
}
td {
padding-left: 19px;
font-size: 14px;
color: #000;
word-break: break-all;
}
} }
} }
// Notice Content Section // Notice Content Section
.notice-detail-content { .notice-detail-content {
padding: 20px 0 60px 0;
border-bottom: 1px solid #eee;
font-size: 16px;
line-height: 1.6;
color: #333;
margin-bottom: $spacing-3xl; margin-bottom: $spacing-3xl;
min-height: 300px;
} }
.notice-content-body { .notice-content-body {
background: #F6F9FB; font-size: 16px;
border-radius: 20px; color: #333;
padding: $spacing-2xl; line-height: 1.6;
min-height: 280px; min-height: 280px;
font-size: $font-size-base;
color: $text-dark;
line-height: $line-height-loose;
@media (max-width: $breakpoint-md) { @media (max-width: $breakpoint-md) {
padding: $spacing-lg;
min-height: 200px; min-height: 200px;
border-radius: 12px;
} }
// Rich text content styling // Rich text content styling
@@ -210,7 +304,8 @@
} }
} }
ul, ol { ul,
ol {
margin: $spacing-md 0; margin: $spacing-md 0;
padding-left: $spacing-xl; padding-left: $spacing-xl;
} }
@@ -218,17 +313,13 @@
li { li {
margin-bottom: $spacing-sm; margin-bottom: $spacing-sm;
} }
@include respond-to('sm') {
background: $white;
padding: 0;
}
} }
// Notice Detail Actions // Notice Detail Actions
.notice-detail-actions { .notice-detail-actions {
display: flex; display: flex;
justify-content: center; justify-content: center;
margin-top: 40px;
} }
// List Button - Figma Design // List Button - Figma Design
@@ -236,32 +327,32 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 200px; width: 247px;
height: 60px; height: 60px;
background: #E5E7EB; background: #2a69de;
color: #fff;
border: none; border: none;
border-radius: 12px; font-size: 20px;
font-family: $font-family-primary; font-weight: 700;
font-size: 18px; border-radius: 10px;
font-weight: $font-weight-bold;
color: #5F666C;
cursor: pointer; cursor: pointer;
transition: $transition-base; transition: $transition-base;
@media (max-width: $breakpoint-md) { @media (max-width: $breakpoint-md) {
width: auto; width: auto;
min-width: 80px; min-width: 120px;
padding: 0 16px; padding: 0 20px;
height: 44px; height: 48px;
font-size: 15px; font-size: 16px;
border-radius: 8px;
} }
&:hover { &:hover {
background: darken(#E5E7EB, 5%); background: darken(#2a69de, 10%);
transform: translateY(-2px);
} }
&:active { &:active {
transform: scale(0.98); transform: scale(0.98);
} }
} }
@@ -402,7 +493,12 @@
} }
} }
h1, h2, h3, h4, h5, h6 { h1,
h2,
h3,
h4,
h5,
h6 {
margin-top: $spacing-xl; margin-top: $spacing-xl;
margin-bottom: $spacing-md; margin-bottom: $spacing-md;
font-weight: $font-weight-semibold; font-weight: $font-weight-semibold;
@@ -425,7 +521,8 @@
} }
} }
ul, ol { ul,
ol {
margin: $spacing-md 0; margin: $spacing-md 0;
padding-left: $spacing-xl; padding-left: $spacing-xl;
} }
@@ -700,9 +797,8 @@
} }
.inquiry-content-body { .inquiry-content-body {
background: #F6F9FB;
border-radius: 20px; border-radius: 20px;
padding: $spacing-2xl; padding: 10px 20px;
min-height: 200px; min-height: 200px;
font-size: $font-size-base; font-size: $font-size-base;
color: $text-dark; color: $text-dark;
File diff suppressed because it is too large Load Diff
@@ -13,6 +13,7 @@
margin: 0 auto; margin: 0 auto;
padding: $spacing-3xl 0; padding: $spacing-3xl 0;
margin-bottom: $spacing-2xl; margin-bottom: $spacing-2xl;
min-height: 750px; // Ensure the footer doesn't float when content is short
@media (max-width: $breakpoint-sm) { @media (max-width: $breakpoint-sm) {
padding: $spacing-lg $spacing-md; padding: $spacing-lg $spacing-md;
@@ -67,8 +68,25 @@
// Extends .list-table from components/_tables.scss // Extends .list-table from components/_tables.scss
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
.user-management-container { .user-management-container {
.list-table { .board-table {
margin-bottom: $spacing-2xl; margin-bottom: $spacing-2xl;
overflow-x: auto;
// Allow table rows to expand if buttons wrap
.board-table-row {
height: auto;
min-height: 50px;
.row-cell.row-actions {
white-space: normal;
}
}
.actions-desktop {
flex-wrap: wrap;
justify-content: center;
gap: 6px;
}
// Link styles in table cells // Link styles in table cells
.row-cell a { .row-cell a {
@@ -82,6 +100,39 @@
} }
} }
// Grid layout for user list table to avoid inline styles
.board-table--user-list {
.board-table-header,
.board-table-row {
display: grid;
grid-template-columns: 24px 112px 1fr 100px 100px 287px;
padding: 0 29px; // Match standard board-table padding
gap: 10px;
@media (max-width: $breakpoint-sm) {
display: flex;
padding: 15px;
min-width: auto;
}
}
.header-cell,
.row-cell {
padding: 0;
width: 100%;
box-sizing: border-box;
&:first-child {
padding-left: 0;
}
&:last-child {
padding-right: 0;
}
}
}
.table-controls { .table-controls {
@extend .common-title-bar; @extend .common-title-bar;
@@ -92,7 +143,14 @@
.search-box { .search-box {
.btn { .btn {
height: 44px; height: 48px;
background-color: #2a69de;
border: 1px solid #2a69de;
border-radius: 9px;
color: #fff;
font-size: 15px;
font-weight: 700;
cursor: pointer;
} }
} }
} }
@@ -101,15 +159,22 @@
// Current user badge (used in actions column) // Current user badge (used in actions column)
.user-current-badge { .user-current-badge {
display: inline-flex; display: inline-flex;
height: 30px;
align-items: center; align-items: center;
gap: 6px; justify-content: center;
padding: 0 $spacing-sm; padding: 0 16px;
background-color: rgba($accent-purple, 0.1); height: 28px;
color: $accent-purple; background-color: #ecf0fa;
border-radius: $border-radius-sm; color: #2a69de;
font-size: 13px; border: 1px solid #4685ef;
font-weight: $font-weight-medium; border-radius: 5px;
font-size: 12px;
font-weight: 400;
font-family: 'Spoqa Han Sans Neo', sans-serif;
gap: 4px;
i {
display: none; // Hide the icon to match Figma exactly
}
// 모바일에서 숨김 // 모바일에서 숨김
@media (max-width: $breakpoint-sm) { @media (max-width: $breakpoint-sm) {
@@ -339,9 +404,12 @@
} }
@keyframes pulse { @keyframes pulse {
0%, 100% {
0%,
100% {
opacity: 1; opacity: 1;
} }
50% { 50% {
opacity: 0.5; opacity: 0.5;
} }
@@ -415,6 +483,7 @@
// 사용자 관리 모바일 디자인 // 사용자 관리 모바일 디자인
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@media (max-width: $breakpoint-sm) { @media (max-width: $breakpoint-sm) {
// Show mobile actions, hide desktop actions // Show mobile actions, hide desktop actions
.actions-desktop { .actions-desktop {
display: none !important; display: none !important;
@@ -659,7 +728,8 @@
width: 20px; width: 20px;
height: 20px; height: 20px;
i, svg { i,
svg {
font-size: 14px; font-size: 14px;
width: 14px; width: 14px;
height: 14px; height: 14px;
@@ -693,6 +763,7 @@
.list-table { .list-table {
.list-table-header { .list-table-header {
.header-cell { .header-cell {
// NO 컬럼 - 모바일에서 숨김 // NO 컬럼 - 모바일에서 숨김
&:nth-child(1) { &:nth-child(1) {
display: none; display: none;
@@ -729,6 +800,7 @@
.list-table-row { .list-table-row {
.row-cell { .row-cell {
// NO 컬럼 - 모바일에서 숨김 // NO 컬럼 - 모바일에서 숨김
&:nth-child(1), &:nth-child(1),
&.row-cell--number { &.row-cell--number {
@@ -754,7 +826,7 @@
display: none !important; display: none !important;
} }
> span:not(.notice-number):not(.file-icon) { >span:not(.notice-number):not(.file-icon) {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@@ -840,6 +912,7 @@
.list-table { .list-table {
.list-table-header { .list-table-header {
.header-cell { .header-cell {
// NO 컬럼 - 모바일에서 숨김 // NO 컬럼 - 모바일에서 숨김
&:nth-child(1) { &:nth-child(1) {
display: none; display: none;
@@ -862,6 +935,7 @@
.list-table-row { .list-table-row {
.row-cell { .row-cell {
// NO 컬럼 - 모바일에서 숨김 // NO 컬럼 - 모바일에서 숨김
&:nth-child(1), &:nth-child(1),
&.row-cell--number { &.row-cell--number {
@@ -886,7 +960,7 @@
display: none !important; display: none !important;
} }
> span:not(.notice-number):not(.file-icon) { >span:not(.notice-number):not(.file-icon) {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@@ -1054,7 +1128,7 @@
background: #e0efff; background: #e0efff;
} }
& + .dropdown-item { &+.dropdown-item {
border-top: 1px solid #eee; border-top: 1px solid #eee;
} }
@@ -1,42 +1,43 @@
<!doctype html> <!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/djbank_title_layout}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/djbank_title_layout}">
<body> <body>
<th:block layout:fragment="title"> <th:block layout:fragment="contentFragment">
<div class="page-title-banner" style="margin-bottom: 24px;"> <!-- Hero Section -->
<img th:src="@{/img/img_title_bg.png}" class="title-image"> <section class="service-hero">
<h1>API 마켓</h1> <div class="service-hero__inner">
</div> <div class="service-hero__icon-wrapper">
</th:block> <img th:src="@{/img/keyimage/api_img.svg}" alt="OPEN API 3D 아이콘"
style="width: 100%; height: 100%; object-fit: contain;" />
</div>
<div class="service-hero__content">
<div class="service-hero__badge">
<span class="service-hero__badge-dot"></span>
<span class="service-hero__badge-text">OPEN API 목록</span>
</div>
<h1 class="service-hero__title">OPEN API</h1>
<p class="service-hero__desc">비즈니스 확장을 위한 DJBank의 핵심 API 인프라를 제공합니다.<br>원하는 API를 선택하여 상세 가이드를 확인하고, 테스트 키를
발급받아 지금 바로 개발을 시작해 보세요.</p>
</div>
</div>
</section>
<th:block layout:fragment="contentFragment"> <section class="api-market-container">
<section class="api-market-container">
<!-- Sidebar Navigation --> <!-- Sidebar Navigation -->
<aside class="api-market-sidebar" id="apiSidebar"> <aside class="service-sidebar" id="apiSidebar">
<!-- Sidebar Header Image --> <nav class="service-nav">
<div class="api-sidebar-header">
<img th:src="@{/img/api_sidebar.png}" alt="API">
</div>
<nav class="api-sidebar-nav">
<!-- All APIs --> <!-- All APIs -->
<div class="menu-section"> <div class="service-nav__item js-menu-trigger" style="cursor: pointer;"
<div class="menu-title" th:classappend="${selected == '-1'} ? 'active'" data-group=""> th:classappend="${selected == '-1' || selected == null} ? 'service-nav__item--active' : ''" data-group="">
전체 전체
</div>
</div> </div>
<!-- Service Categories --> <!-- Service Categories -->
<div class="menu-section" th:each="service : ${services}"> <div class="service-nav__item js-menu-trigger" style="cursor: pointer;" th:each="service : ${services}"
<div class="menu-title" th:classappend="${selected == service.id} ? 'service-nav__item--active' : ''" th:data-group="${service.id}">
th:classappend="${selected == service.id} ? 'active'" [[${service.groupName}]]
th:data-group="${service.id}">
[[${service.groupName}]]
</div>
</div> </div>
</nav> </nav>
</aside> </aside>
@@ -59,15 +60,15 @@
</div> </div>
<div class="api-market-search"> <div class="api-market-search">
<form id="searchForm" th:action="@{/apis}" method="get"> <form id="searchForm" th:action="@{/apis}" method="get">
<input type="hidden" name="groupIds" th:value="${search.groupIds != null and !search.groupIds.isEmpty() ? search.groupIds[0] : ''}"/> <input type="hidden" name="groupIds"
<input type="text" th:value="${search.groupIds != null and !search.groupIds.isEmpty() ? search.groupIds[0] : ''}" />
class="search-input" <input type="text" class="search-input" name="keyword" th:value="${search.keyword}"
name="keyword" placeholder="검색어를 입력하세요.">
th:value="${search.keyword}"
placeholder="어떤 API를 찾고 계신가요?">
<button type="submit" class="search-submit-btn" aria-label="검색"> <button type="submit" class="search-submit-btn" aria-label="검색">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.23047 0C14.3285 0 18.4618 4.0539 18.4619 9.05469C18.4619 11.1083 17.7634 13.0014 16.5889 14.5205C16.5913 14.5229 16.5942 14.5249 16.5967 14.5273L19.5498 17.4238C20.1502 18.0131 20.1501 18.9683 19.5498 19.5576C18.949 20.147 17.9748 20.147 17.374 19.5576L14.4209 16.6621C14.3966 16.6383 14.3749 16.6119 14.3525 16.5869C12.8868 17.5478 11.1257 18.1094 9.23047 18.1094C4.13258 18.1092 0 14.0554 0 9.05469C0.000110268 4.05404 4.13265 0.000222701 9.23047 0ZM9.23047 3.01855C5.83201 3.01878 3.07726 5.721 3.07715 9.05469C3.07715 12.3885 5.83194 15.0916 9.23047 15.0918C12.6292 15.0918 15.3848 12.3886 15.3848 9.05469C15.3847 5.72086 12.6291 3.01855 9.23047 3.01855Z" fill="#515961"/> <path
d="M9.23047 0C14.3285 0 18.4618 4.0539 18.4619 9.05469C18.4619 11.1083 17.7634 13.0014 16.5889 14.5205C16.5913 14.5229 16.5942 14.5249 16.5967 14.5273L19.5498 17.4238C20.1502 18.0131 20.1501 18.9683 19.5498 19.5576C18.949 20.147 17.9748 20.147 17.374 19.5576L14.4209 16.6621C14.3966 16.6383 14.3749 16.6119 14.3525 16.5869C12.8868 17.5478 11.1257 18.1094 9.23047 18.1094C4.13258 18.1092 0 14.0554 0 9.05469C0.000110268 4.05404 4.13265 0.000222701 9.23047 0ZM9.23047 3.01855C5.83201 3.01878 3.07726 5.721 3.07715 9.05469C3.07715 12.3885 5.83194 15.0916 9.23047 15.0918C12.6292 15.0918 15.3848 12.3886 15.3848 9.05469C15.3847 5.72086 12.6291 3.01855 9.23047 3.01855Z"
fill="#515961" />
</svg> </svg>
</button> </button>
</form> </form>
@@ -76,34 +77,26 @@
<!-- API Cards Grid --> <!-- API Cards Grid -->
<div class="api-card-grid" th:if="${apis != null and !apis.isEmpty()}"> <div class="api-card-grid" th:if="${apis != null and !apis.isEmpty()}">
<div class="api-card" <div class="api-card" th:each="api : ${apis}" th:data-href="@{/apis/detail(id=${api.apiId})}" role="button"
th:each="api : ${apis}" tabindex="0">
th:data-href="@{/apis/detail(id=${api.apiId})}"
role="button" <!-- Group Badge -->
tabindex="0"> <div class="api-card-badge">
<!-- Group Info --> <span th:text="${api.apiGroupName}">그룹 이름</span>
<div class="api-card-group">
<div class="api-card-group-icon">
<img th:if="${api.mainIcon}"
th:src="${api.mainIcon}"
th:alt="${api.apiGroupName}"
onerror="this.style.display='none'; this.parentElement.innerHTML='<i class=\'fas fa-cube\'></i>'">
<i th:if="${#strings.isEmpty(api.mainIcon)}" class="fas fa-cube"></i>
</div>
<span class="api-card-group-name" th:text="${api.apiGroupName}">
그룹 이름
</span>
</div> </div>
<!-- API Name --> <!-- API Name -->
<h3 class="api-card-name" th:text="${api.apiName}"> <h3 class="api-card-title" th:text="${api.apiName}">API 이름</h3>
API 이름
</h3>
<!-- API Description --> <!-- API Description -->
<p class="api-card-description" th:text="${api.apiSimpleDescription ?: 'API 설명이 없습니다.'}"> <p class="api-card-desc" th:text="${api.apiSimpleDescription ?: 'API 설명이 없습니다.'}">API 설명</p>
API 설명
</p> <!-- API Image (Bottom Right) -->
<div class="api-card-image">
<img th:if="${api.mainIcon}" th:src="${api.mainIcon}" th:alt="${api.apiGroupName}"
onerror="this.style.display='none'">
</div>
</div> </div>
</div> </div>
@@ -115,14 +108,14 @@
</div> </div>
</main> </main>
</section> </section>
</th:block> </th:block>
</body> </body>
<th:block layout:fragment="contentScript"> <th:block layout:fragment="contentScript">
<script th:inline="javascript"> <script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function () {
// DOM Elements // DOM Elements
const sidebar = document.getElementById('apiSidebar'); const sidebar = document.getElementById('apiSidebar');
const mobileToggle = document.getElementById('mobileToggle'); const mobileToggle = document.getElementById('mobileToggle');
@@ -133,9 +126,9 @@
// API Card Click Handlers // API Card Click Handlers
const apiCards = document.querySelectorAll('.api-card'); const apiCards = document.querySelectorAll('.api-card');
apiCards.forEach(function(card) { apiCards.forEach(function (card) {
// Click handler // Click handler
card.addEventListener('click', function() { card.addEventListener('click', function () {
const href = this.getAttribute('data-href'); const href = this.getAttribute('data-href');
if (href) { if (href) {
window.location.href = href; window.location.href = href;
@@ -143,7 +136,7 @@
}); });
// Keyboard accessibility // Keyboard accessibility
card.addEventListener('keypress', function(e) { card.addEventListener('keypress', function (e) {
if (e.key === 'Enter' || e.key === ' ') { if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault(); e.preventDefault();
const href = this.getAttribute('data-href'); const href = this.getAttribute('data-href');
@@ -155,9 +148,9 @@
}); });
// Category/Service Selection // Category/Service Selection
const menuTitles = document.querySelectorAll('.menu-title'); const menuTitles = document.querySelectorAll('.js-menu-trigger');
menuTitles.forEach(function(title) { menuTitles.forEach(function (title) {
title.addEventListener('click', function(e) { title.addEventListener('click', function (e) {
e.preventDefault(); e.preventDefault();
const groupId = this.getAttribute('data-group'); const groupId = this.getAttribute('data-group');
@@ -175,7 +168,7 @@
}); });
// Search on Enter key // Search on Enter key
searchInput.addEventListener('keypress', function(e) { searchInput.addEventListener('keypress', function (e) {
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
searchForm.submit(); searchForm.submit();
@@ -184,7 +177,7 @@
// Mobile Menu Toggle // Mobile Menu Toggle
if (mobileToggle && sidebar && mobileOverlay) { if (mobileToggle && sidebar && mobileOverlay) {
mobileToggle.addEventListener('click', function(e) { mobileToggle.addEventListener('click', function (e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@@ -202,14 +195,14 @@
}); });
// Add touch event for better mobile support // Add touch event for better mobile support
mobileToggle.addEventListener('touchend', function(e) { mobileToggle.addEventListener('touchend', function (e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.click(); this.click();
}); });
// Close menu on overlay click // Close menu on overlay click
mobileOverlay.addEventListener('click', function() { mobileOverlay.addEventListener('click', function () {
sidebar.classList.remove('mobile-open'); sidebar.classList.remove('mobile-open');
this.classList.remove('active'); this.classList.remove('active');
mobileToggle.innerHTML = '☰'; mobileToggle.innerHTML = '☰';
@@ -217,8 +210,8 @@
}); });
// Close menu on category selection (mobile) // Close menu on category selection (mobile)
menuTitles.forEach(function(title) { menuTitles.forEach(function (title) {
title.addEventListener('click', function() { title.addEventListener('click', function () {
if (window.innerWidth <= 768) { if (window.innerWidth <= 768) {
sidebar.classList.remove('mobile-open'); sidebar.classList.remove('mobile-open');
mobileOverlay.classList.remove('active'); mobileOverlay.classList.remove('active');
@@ -236,7 +229,7 @@
} }
// Close mobile menu on resize to desktop // Close mobile menu on resize to desktop
window.addEventListener('resize', function() { window.addEventListener('resize', function () {
if (window.innerWidth > 768 && sidebar.classList.contains('mobile-open')) { if (window.innerWidth > 768 && sidebar.classList.contains('mobile-open')) {
sidebar.classList.remove('mobile-open'); sidebar.classList.remove('mobile-open');
mobileOverlay.classList.remove('active'); mobileOverlay.classList.remove('active');
@@ -1,86 +1,115 @@
<!doctype html> <!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" <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_title_layout}"> xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/djbank_title_layout}">
<body> <body>
<section layout:fragment="title"> <th:block layout:fragment="contentFragment">
<div class="page-title-banner"> <div class="signup-guide-v2">
<img th:src="@{/img/img_title_bg.png}" class="title-image" alt="타이틀 배경"> <!-- Hero Section -->
<h1>FAQ</h1> <section class="service-hero">
</div> <div class="service-hero__inner">
</section> <div class="service-hero__icon-wrapper">
<!-- TODO: Update icon to faq icon -->
<th:block layout:fragment="contentFragment"> <img th:src="@{/img/keyimage/faq_img.svg}" alt="FAQ 아이콘"
<section class="user-management-container"> style="width: 100%; height: 100%; object-fit: contain;" />
</div>
<!-- Search and Filter Controls --> <div class="service-hero__content">
<form name="faqForm" th:action="@{/faq_list}" method="post" th:object="${search}" onsubmit="return false;"> <div class="service-hero__badge">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"> <span class="service-hero__badge-dot"></span>
<input type="hidden" name="page" value="1"> <span class="service-hero__badge-text">FAQ 게시판</span>
</div>
<div class="table-controls"> <h1 class="service-hero__title">FAQ</h1>
<h2 class="total-count"> <p class="service-hero__desc">서비스 이용 중 궁금한 사항에 대한 답변을 빠르게 확인해 보세요<br>카테고리별 분류와 검색을 통해 원하는 정보를 쉽게 찾아보실 수
<strong th:text="${page.totalElements}">0</strong> 있습니다.</p>
</h2>
<div class="search-field">
<input type="text"
th:field="*{searchWrd}"
placeholder="질문 또는 답변 내용으로 검색"
onkeypress="if(event.keyCode === 13) { fn_search_faq(); return false; }">
<button type="button" class="search-field-btn" onclick="fn_search_faq()">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.5 17.5L13.875 13.875M15.8333 9.16667C15.8333 12.8486 12.8486 15.8333 9.16667 15.8333C5.48477 15.8333 2.5 12.8486 2.5 9.16667C2.5 5.48477 5.48477 2.5 9.16667 2.5C12.8486 2.5 15.8333 5.48477 15.8333 9.16667Z" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span class="blind">검색</span>
</button>
</div>
</div>
</form>
<!-- FAQ Accordion -->
<div class="faq-accordion" th:if="${!#lists.isEmpty(page.content)}">
<div class="faq-item" th:each="faq : ${page.content}">
<div class="faq-question">
<span class="faq-question-text" th:text="${faq.faqQuestion}">질문 내용이 여기에 표시됩니다.</span>
<span class="faq-icon">
<svg width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1L7 7L13 1" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
</div>
<div class="faq-answer">
<div class="faq-answer-content editor-content" th:utext="${faq.faqAnswer}">
답변 내용이 여기에 표시됩니다.
</div> </div>
</div> </div>
</section>
<!-- Main Content -->
<div class="service-main">
<!-- Sidebar -->
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('faq')}"></th:block>
<!-- Content -->
<section class="service-content">
<div class="notice-list-container"
style="padding: 0; min-height: auto; margin: 0; background: transparent; box-shadow: none;">
<!-- Search and Filter Controls -->
<form name="faqForm" th:action="@{/faq_list}" method="post" th:object="${search}" onsubmit="return false;">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<input type="hidden" name="page" value="1">
<div class="table-controls">
<h2 class="total-count">
<strong th:text="${page.totalElements}">0</strong>
</h2>
<div class="search-field">
<input type="text" th:field="*{searchWrd}" placeholder="질문 또는 답변 내용으로 검색"
onkeypress="if(event.keyCode === 13) { fn_search_faq(); return false; }">
<button type="button" class="search-field-btn" onclick="fn_search_faq()">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M17.5 17.5L13.875 13.875M15.8333 9.16667C15.8333 12.8486 12.8486 15.8333 9.16667 15.8333C5.48477 15.8333 2.5 12.8486 2.5 9.16667C2.5 5.48477 5.48477 2.5 9.16667 2.5C12.8486 2.5 15.8333 5.48477 15.8333 9.16667Z"
stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<span class="blind">검색</span>
</button>
</div>
</div>
</form>
<!-- FAQ Accordion -->
<div class="faq-accordion" th:if="${!#lists.isEmpty(page.content)}">
<div class="faq-item" th:each="faq : ${page.content}">
<div class="faq-question">
<span class="faq-question-q"
style="font-family: 'OneShinhan', sans-serif; font-size: 20px; font-weight: 700; color: #555; margin-right: 15px;">Q</span>
<span class="faq-question-text" th:text="${faq.faqQuestion}">질문 내용이 여기에 표시됩니다.</span>
<span class="faq-icon">
<svg width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1L7 7L13 1" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</span>
</div>
<div class="faq-answer">
<div class="faq-answer-content editor-content" th:utext="${faq.faqAnswer}">
답변 내용이 여기에 표시됩니다.
</div>
</div>
</div>
</div>
<!-- Empty State -->
<div class="user-empty-state" th:if="${#lists.isEmpty(page.content)}">
<div class="empty-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120" fill="none"
role="img" aria-label="데이터 없음">
<circle cx="60" cy="60" r="50" fill="#F3F4F6" />
<rect x="30" y="35" width="60" height="50" rx="4" fill="#E5E7EB" stroke="#D1D5DB" stroke-width="2" />
<line x1="40" y1="50" x2="80" y2="50" stroke="#9CA3AF" stroke-width="2" stroke-linecap="round" />
<line x1="40" y1="60" x2="72" y2="60" stroke="#9CA3AF" stroke-width="2" stroke-linecap="round" />
<line x1="40" y1="70" x2="65" y2="70" stroke="#9CA3AF" stroke-width="2" stroke-linecap="round" />
<circle cx="85" cy="80" r="14" fill="#F9FAFB" stroke="#D1D5DB" stroke-width="2" />
<line x1="95" y1="90" x2="102" y2="97" stroke="#D1D5DB" stroke-width="3" stroke-linecap="round" />
<line x1="81" y1="80" x2="89" y2="80" stroke="#9CA3AF" stroke-width="2" stroke-linecap="round" />
</svg>
</div>
<h3>조회된 FAQ가 없습니다</h3>
<p>검색 조건을 변경하여 다시 시도해주세요.</p>
</div>
<!-- Pagination -->
<div class="pagination" th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}">
</div>
</div>
</section>
</div> </div>
</div> </div>
</th:block>
<!-- Empty State -->
<div class="user-empty-state" th:if="${#lists.isEmpty(page.content)}">
<div class="empty-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120" fill="none" role="img" aria-label="데이터 없음">
<circle cx="60" cy="60" r="50" fill="#F3F4F6"/>
<rect x="30" y="35" width="60" height="50" rx="4" fill="#E5E7EB" stroke="#D1D5DB" stroke-width="2"/>
<line x1="40" y1="50" x2="80" y2="50" stroke="#9CA3AF" stroke-width="2" stroke-linecap="round"/>
<line x1="40" y1="60" x2="72" y2="60" stroke="#9CA3AF" stroke-width="2" stroke-linecap="round"/>
<line x1="40" y1="70" x2="65" y2="70" stroke="#9CA3AF" stroke-width="2" stroke-linecap="round"/>
<circle cx="85" cy="80" r="14" fill="#F9FAFB" stroke="#D1D5DB" stroke-width="2"/>
<line x1="95" y1="90" x2="102" y2="97" stroke="#D1D5DB" stroke-width="3" stroke-linecap="round"/>
<line x1="81" y1="80" x2="89" y2="80" stroke="#9CA3AF" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
<h3>조회된 FAQ가 없습니다</h3>
<p>검색 조건을 변경하여 다시 시도해주세요.</p>
</div>
<!-- Pagination -->
<div class="pagination" th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}">
</div>
</section>
</th:block>
</body> </body>
@@ -102,15 +131,15 @@
return textArea.value; return textArea.value;
} }
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function () {
// Decode HTML entities in FAQ answers // Decode HTML entities in FAQ answers
document.querySelectorAll('.faq-answer-content').forEach(function(element) { document.querySelectorAll('.faq-answer-content').forEach(function (element) {
element.innerHTML = decodeHTMLEntities(element.innerHTML); element.innerHTML = decodeHTMLEntities(element.innerHTML);
}); });
// FAQ Accordion functionality // FAQ Accordion functionality
document.querySelectorAll('.faq-question').forEach(function(question) { document.querySelectorAll('.faq-question').forEach(function (question) {
question.addEventListener('click', function(e) { question.addEventListener('click', function (e) {
e.preventDefault(); e.preventDefault();
const faqItem = this.closest('.faq-item'); const faqItem = this.closest('.faq-item');
@@ -1,78 +1,118 @@
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" <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_title_layout}"> xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/djbank_title_layout}">
<body> <body>
<section layout:fragment="title"> <section layout:fragment="title">
<div class="page-title-banner"> <div class="page-title-banner">
<img th:src="@{/img/img_title_bg.png}" class="title-image" alt="타이틀 배경"> <img th:src="@{/img/img_title_bg.png}" class="title-image" alt="타이틀 배경">
<h1>Q&A</h1> <h1>Q&A</h1>
</div> </div>
</section> </section>
<section layout:fragment="contentFragment"> <section layout:fragment="contentFragment">
<div class="inquiry-detail-container"> <div class="signup-guide-v2">
<!-- Hero Section -->
<!-- Inquiry Header: Status Badge and Title --> <section class="service-hero">
<div class="inquiry-detail-header"> <div class="service-hero__inner">
<div class="inquiry-header-top"> <div class="service-hero__icon-wrapper">
<span class="inquiry-status-badge" <img th:src="@{/img/keyimage/q&a_img.svg}" alt="Q&A 아이콘"
th:classappend="${inquiry.inquiryStatus == 'RESPONDED' ? 'inquiry-status-badge--completed' : (inquiry.inquiryStatus == 'CLOSED' ? 'inquiry-status-badge--closed' : 'inquiry-status-badge--pending')}" style="width: 100%; height: 100%; object-fit: contain;" />
th:text="${inquiry.inquiryStatus == 'RESPONDED' ? '답변완료' : (inquiry.inquiryStatus == 'CLOSED' ? '종료' : '답변대기')}"> </div>
답변대기 <div class="service-hero__content">
</span> <div class="service-hero__badge">
<div class="inquiry-header-right"> <span class="service-hero__badge-dot"></span>
<span class="inquiry-detail-writer" <span class="service-hero__badge-text">Q&A 게시판</span>
th:text="${inquiry.inquirerName != null ? inquiry.inquirerName : (inquiry.inquirer != null ? inquiry.inquirer.userName : '')}">작성자</span> </div>
<span class="inquiry-detail-divider">|</span> <h1 class="service-hero__title">Q&A</h1>
<span class="inquiry-detail-date" th:text="${#temporals.format(inquiry.createdDate, 'yyyy.MM.dd')}">2025.11.01</span> <p class="service-hero__desc">DJBank 오픈 API 이용 중 발생한 의문점이나 불편 사항을 보내주세요<br>접수해주신 문의 사항은 담당자 확인 후 빠르게 안내해
드리겠습니다.</p>
</div>
</div> </div>
</div> </section>
<h2 class="inquiry-detail-title" th:text="${inquiry.inquirySubject}">질문 제목</h2>
</div>
<!-- Inquiry Content Section --> <!-- Main Content -->
<div class="inquiry-detail-content"> <div class="service-main">
<div id="inquiryDetail" class="inquiry-content-body" th:utext="${inquiry.inquiryDetail}"> <!-- Sidebar -->
문의 내용이 여기에 표시됩니다. <th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('qna')}"></th:block>
<!-- Content -->
<section class="service-content">
<div class="inquiry-detail-container">
<!-- Detail Header -->
<div class="notice-detail-header">
<div class="inquiry-header-meta">
<span th:text="${#temporals.format(inquiry.createdDate, 'yyyy.MM.dd')}">2026.06.24</span>
<span>|</span>
<span
th:text="${inquiry.inquirerName != null ? inquiry.inquirerName : (inquiry.inquirer != null ? inquiry.inquirer.userName : '')}">EACTIVE</span>
</div>
<div class="notice-detail-title-wrapper">
<span class="notice-type-badge notice-type-badge--completed"
th:if="${inquiry.inquiryStatus == 'RESPONDED'}">답변완료</span>
<span class="notice-type-badge notice-type-badge--closed"
th:if="${inquiry.inquiryStatus == 'CLOSED'}">종료</span>
<span class="notice-type-badge notice-type-badge--pending"
th:if="${inquiry.inquiryStatus != 'RESPONDED' and inquiry.inquiryStatus != 'CLOSED'}">답변대기</span>
<h2 class="notice-detail-title" th:text="${inquiry.inquirySubject}">질문 제목</h2>
</div>
</div>
<!-- Detail Body -->
<div class="notice-detail-body">
<div id="inquiryDetail" class="inquiry-content-body" th:utext="${inquiry.inquiryDetail}">
문의 내용이 여기에 표시됩니다.
</div>
</div>
<!-- Admin Response Section -->
<div class="inquiry-response-section"
th:if="${inquiry.inquiryStatus == 'RESPONDED' or inquiry.inquiryStatus == 'CLOSED'}">
<div class="inquiry-response-header">
<div class="response-admin">
<span class="admin-label">ADMIN</span>
<span class="admin-badge">관리자</span>
</div>
<div class="response-meta">
<span th:text="${#temporals.format(inquiry.responseDate, 'yyyy.MM.dd')}">2026-06-24</span>
</div>
</div>
<div id="responseDetail" class="inquiry-response-body" th:utext="${inquiry.responseDetail}">
답변 내용이 여기에 표시됩니다.
</div>
</div>
<!-- Comment Section (DJBank Custom DJPGPT0002) -->
<th:block
th:replace="~{apps/community/djb/fragments-inquiry-comments :: commentSection(${inquiry}, ${commentWritable})}">
</th:block>
<!-- Action Buttons -->
<div class="inquiry-actions">
<button type="button" class="btn-inquiry-new" th:onclick="|location.href='@{/inquiry}'|">목록</button>
<th:block
th:if="${inquiry.inquiryStatus == 'PENDING' && inquiry.inquirer.id == #authentication.principal.id}">
<form id="deleteForm" th:action="@{/inquiry/{id}/delete(id=${inquiry.id})}" method="post"
style="display: none;"></form>
<button type="button" class="btn-inquiry-new btn-inquiry-new--outline"
th:onclick="|location.href='@{/inquiry/edit(id=${inquiry.id})}'|">수정</button>
<button type="button" class="btn-inquiry-new btn-inquiry-new--danger"
onclick="confirmDelete()">삭제</button>
</th:block>
</div>
</div>
</section>
</div> </div>
</div> </div>
</section>
<!-- Admin Response Section -->
<div class="inquiry-response-section" th:if="${inquiry.inquiryStatus == 'RESPONDED' or inquiry.inquiryStatus == 'CLOSED'}">
<div class="inquiry-response-header">
<span class="response-label">관리자 답변</span>
<span class="response-date" th:text="${#temporals.format(inquiry.responseDate, 'yyyy.MM.dd')}">2025.11.02</span>
</div>
<div class="inquiry-response-body" id="responseDetail" th:utext="${inquiry.responseDetail}">
답변 내용이 여기에 표시됩니다.
</div>
</div>
<!-- Comment Section (DJBank Custom DJPGPT0002) -->
<th:block th:replace="~{apps/community/djb/fragments-inquiry-comments :: commentSection(${inquiry}, ${commentWritable})}"></th:block>
<!-- Action Buttons -->
<div class="inquiry-detail-actions">
<div class="actions-left">
<button type="button" class="btn-inquiry-list" th:onclick="|location.href='@{/inquiry}'|">목록</button>
</div>
<div class="actions-right" th:if="${inquiry.inquiryStatus == 'PENDING' && inquiry.inquirer.id == #authentication.principal.id}">
<form id="deleteForm" th:action="@{/inquiry/{id}/delete(id=${inquiry.id})}" method="post" style="display: none;"></form>
<button type="button" class="btn-inquiry-edit" th:onclick="|location.href='@{/inquiry/edit(id=${inquiry.id})}'|">수정</button>
<button type="button" class="btn-inquiry-delete" onclick="confirmDelete()">삭제</button>
</div>
</div>
</div>
</section>
</body> </body>
<th:block layout:fragment="contentScript"> <th:block layout:fragment="contentScript">
<script th:inline="javascript"> <script th:inline="javascript">
$(document).ready(function() { $(document).ready(function () {
var successMsg = [[${success}]]; var successMsg = [[${ success }]];
if (successMsg) { if (successMsg) {
customPopups.showAlert(successMsg); customPopups.showAlert(successMsg);
} }
@@ -87,14 +127,14 @@
} }
function confirmDelete() { function confirmDelete() {
customPopups.showConfirm('문의글을 삭제하시겠습니까?', function(result) { customPopups.showConfirm('문의글을 삭제하시겠습니까?', function (result) {
if (result) { if (result) {
document.getElementById('deleteForm').submit(); document.getElementById('deleteForm').submit();
} }
}); });
} }
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function () {
var inquiryElement = document.getElementById('inquiryDetail'); var inquiryElement = document.getElementById('inquiryDetail');
if (inquiryElement) { if (inquiryElement) {
inquiryElement.innerHTML = decodeHTMLEntities(inquiryElement.innerHTML); inquiryElement.innerHTML = decodeHTMLEntities(inquiryElement.innerHTML);
@@ -11,69 +11,85 @@
</section> </section>
<th:block layout:fragment="contentFragment" th:with="isNew=${inquiry.id == null}"> <th:block layout:fragment="contentFragment" th:with="isNew=${inquiry.id == null}">
<section class="inquiry-form-container"> <div class="signup-guide-v2">
<!-- Hero Section -->
<!-- Title Bar --> <section class="service-hero">
<div class="register-title-bar"> <div class="service-hero__inner">
<h2 class="register-title" th:text="${isNew ? '질문하기' : '질문 수정'}">질문하기</h2> <div class="service-hero__icon-wrapper">
<span class="register-subtitle">궁금하신 사항을 작성해 주세요.</span> <img th:src="@{/img/keyimage/q&a_img.svg}" alt="Q&A 아이콘"
</div> style="width: 100%; height: 100%; object-fit: contain;" />
<!-- Form Content -->
<div class="register-form-container">
<form name="inquiryForm" id="inquiryForm" th:action="${isNew}? @{/inquiry} : @{/inquiry/edit}"
th:object="${inquiry}" method="post" class="register-form">
<input type="hidden" th:field="*{id}" th:if="${!isNew}">
<!-- Subject Field -->
<div class="form-row">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">제목</span>
<span class="required-badge">필수</span>
</div>
<div class="form-field-wrapper">
<input type="text"
id="inquirySubject"
th:field="*{inquirySubject}"
class="form-input"
placeholder="제목을 입력하세요"
required
maxlength="200">
</div>
</div> </div>
<div class="service-hero__content">
<!-- Content Field --> <div class="service-hero__badge">
<div class="form-row"> <span class="service-hero__badge-dot"></span>
<div class="form-label-wrapper"> <span class="service-hero__badge-text">Q&A 게시판</span>
<span class="form-label-text">내용</span>
<span class="required-badge">필수</span>
</div>
<div class="form-field-wrapper">
<textarea id="inquiryDetail"
th:field="*{inquiryDetail}"
class="form-textarea"
rows="10"
placeholder="내용을 입력하세요"
required
maxlength="2000"></textarea>
<small class="char-counter-wrapper">
<span class="char-counter">0 / 2000</span>
</small>
</div> </div>
<h1 class="service-hero__title">Q&A</h1>
<p class="service-hero__desc">DJBank 오픈 API 이용 중 발생한 의문점이나 불편 사항을 보내주세요<br>접수해주신 문의 사항은 담당자 확인 후 빠르게 안내해 드리겠습니다.</p>
</div> </div>
</div>
</section>
</form> <!-- Main Content -->
<div class="service-main">
<!-- Sidebar -->
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('qna')}"></th:block>
<!-- Content -->
<section class="service-content">
<div class="djb-board-write-container">
<!-- Title Bar -->
<div class="djb-board-write-header">
<h2 class="title" th:text="${isNew ? '질문하기' : '질문 수정'}">질문하기</h2>
<p class="subtitle">궁금하신 사항을 작성해 주세요.</p>
</div>
<!-- Form Content -->
<form name="inquiryForm" id="inquiryForm" th:action="${isNew}? @{/inquiry} : @{/inquiry/edit}"
th:object="${inquiry}" method="post" class="djb-board-form">
<input type="hidden" th:field="*{id}" th:if="${!isNew}">
<!-- Subject Field -->
<div class="form-group">
<label class="djb-label" for="inquirySubject">제목 <span class="required">*</span></label>
<input type="text"
id="inquirySubject"
th:field="*{inquirySubject}"
class="djb-input"
placeholder="제목을 입력하세요"
required
maxlength="200">
</div>
<!-- Content Field -->
<div class="form-group">
<label class="djb-label" for="inquiryDetail">내용 <span class="required">*</span></label>
<textarea id="inquiryDetail"
th:field="*{inquiryDetail}"
class="djb-textarea"
placeholder="내용을 입력하세요"
required
maxlength="2000"></textarea>
<div class="djb-char-counter-wrapper">
<span class="char-counter">0 / 2000</span>
</div>
</div>
</form>
<!-- Form Actions -->
<div class="form-actions">
<button type="button" class="btn-cancel" th:onclick="|location.href='@{/inquiry}'|">취소</button>
<button type="button" class="btn-submit btn-submit-form">
<span th:text="${isNew ? '등록' : '수정'}">등록</span>
</button>
</div>
</div>
</section>
</div> </div>
</div>
<!-- Form Actions -->
<div class="form-actions">
<button type="button" class="btn btn-submit btn-secondary" th:onclick="|location.href='@{/inquiry}'|">취소</button>
<button type="button" class="btn btn-submit btn-primary btn-submit-form">
<span th:text="${isNew ? '등록' : '수정'}">등록</span>
</button>
</div>
</section>
</th:block> </th:block>
</body> </body>
@@ -1,112 +1,135 @@
<!doctype html> <!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" <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_title_layout}"> xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/djbank_title_layout}">
<body> <body>
<section layout:fragment="title"> <th:block layout:fragment="contentFragment">
<div class="page-title-banner"> <div class="signup-guide-v2">
<img th:src="@{/img/img_title_bg.png}" class="title-image" alt="타이틀 배경"> <!-- Hero Section -->
<h1>Q&A</h1> <section class="service-hero">
</div> <div class="service-hero__inner">
</section> <div class="service-hero__icon-wrapper">
<!-- Used notice icon as a placeholder, can be updated if inquiry icon exists -->
<th:block layout:fragment="contentFragment"> <img th:src="@{/img/keyimage/notice_img.svg}" alt="Q&A 아이콘"
<section class="user-management-container inquiry-list-container"> style="width: 100%; height: 100%; object-fit: contain;" />
<!-- Search and Filter Controls -->
<form name="inquiryForm" th:action="@{/inquiry}" method="get" onsubmit="return false;">
<input type="hidden" name="page" value="1">
<input name="searchCnd" type="hidden" value="0">
<div class="table-controls">
<h2 class="total-count">
<strong th:text="${page.totalElements}">0</strong>
</h2>
<div class="search-field">
<input type="text"
name="searchWrd"
th:value="${search.searchWrd}"
placeholder="제목 또는 내용으로 검색"
onkeypress="if(event.keyCode === 13) { fn_search_inquiry(); return false; }">
<button type="button" class="search-field-btn" onclick="fn_search_inquiry()">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.5 17.5L13.875 13.875M15.8333 9.16667C15.8333 12.8486 12.8486 15.8333 9.16667 15.8333C5.48477 15.8333 2.5 12.8486 2.5 9.16667C2.5 5.48477 5.48477 2.5 9.16667 2.5C12.8486 2.5 15.8333 5.48477 15.8333 9.16667Z" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span class="blind">검색</span>
</button>
</div>
</div>
</form>
<!-- Inquiry Table -->
<div class="list-table" th:if="${!inquiries.isEmpty()}">
<!-- Table Header -->
<div class="list-table-header">
<div class="header-cell" style="width: 80px;">NO</div>
<div class="header-cell" style="flex: 1; min-width: 200px;">제목</div>
<div class="header-cell" style="width: 100px;">작성자</div>
<div class="header-cell" style="width: 120px;">처리상태</div>
<div class="header-cell" style="width: 120px;">등록일</div>
</div>
<!-- Table Body -->
<div class="list-table-body">
<div class="list-table-row"
th:each="inquiry, status : ${inquiries}"
th:onclick="'location.href=\'' + @{/inquiry/detail(id=${inquiry.id})} + '\''"
style="cursor: pointer;">
<div class="row-cell row-cell--number" style="width: 80px;" data-label="NO"
th:text="${page.totalElements - (page.number * page.size) - status.index}">1</div>
<div class="row-cell row-cell--title" style="flex: 1; min-width: 200px;" data-label="제목">
<a th:href="@{/inquiry/detail(id=${inquiry.id})}" class="notice-title-link">
<span class="notice-number" th:text="|[${page.totalElements - (page.number * page.size) - status.index}]|">[1]</span>
<span th:text="${inquiry.inquirySubject}">질문 제목</span>
<span class="inquiry-comment-count"
th:if="${commentCounts != null and commentCounts.get(inquiry.id) != null and commentCounts.get(inquiry.id) > 0}"
th:text="|[${commentCounts.get(inquiry.id)}]|">[3]</span>
</a>
</div> </div>
<div class="row-cell" style="width: 100px;" data-label="작성자" th:text="${inquiry.maskedInquirerName}">홍**</div> <div class="service-hero__content">
<div class="row-cell" style="width: 120px;" data-label="처리상태"> <div class="service-hero__badge">
<span class="inquiry-status-badge" <span class="service-hero__badge-dot"></span>
th:classappend="${inquiry.inquiryStatus == 'RESPONDED' ? 'inquiry-status-badge--completed' : (inquiry.inquiryStatus == 'CLOSED' ? 'inquiry-status-badge--closed' : 'inquiry-status-badge--pending')}" <span class="service-hero__badge-text">공지사항 목록</span>
th:text="${inquiry.inquiryStatus == 'RESPONDED' ? '답변완료' : (inquiry.inquiryStatus == 'CLOSED' ? '종료' : '답변대기')}"> </div>
답변대기 <h1 class="service-hero__title">Q&A</h1>
</span> <p class="service-hero__desc">DJBank 오픈 API 이용 중 발생한 의문점이나 불편 사항을 보내주세요<br>접수해주신 문의 사항은 담당자 확인 후 빠르게 안내해
드리겠습니다.</p>
</div> </div>
<div class="row-cell" style="width: 120px;" data-label="등록일"
th:text="${#temporals.format(inquiry.createdDate, 'yyyy.MM.dd')}">2025.01.01</div>
</div> </div>
</section>
<!-- Main Content -->
<div class="service-main">
<!-- Sidebar -->
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('qna')}"></th:block>
<!-- Content -->
<section class="service-content">
<section class="user-management-container inquiry-list-container"
style="padding: 0; min-height: auto; margin: 0; background: transparent; box-shadow: none;">
<!-- Search and Filter Controls -->
<form name="inquiryForm" th:action="@{/inquiry}" method="get" onsubmit="return false;">
<input type="hidden" name="page" value="1">
<input name="searchCnd" type="hidden" value="0">
<div class="table-controls">
<h2 class="total-count">
<strong th:text="${page.totalElements}">0</strong>
</h2>
<div class="search-field">
<input type="text" name="searchWrd" th:value="${search.searchWrd}" placeholder="제목/내용 검색"
onkeypress="if(event.keyCode === 13) { fn_search_inquiry(); return false; }">
<button type="button" class="search-field-btn" onclick="fn_search_inquiry()">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M17.5 17.5L13.875 13.875M15.8333 9.16667C15.8333 12.8486 12.8486 15.8333 9.16667 15.8333C5.48477 15.8333 2.5 12.8486 2.5 9.16667C2.5 5.48477 5.48477 2.5 9.16667 2.5C12.8486 2.5 15.8333 5.48477 15.8333 9.16667Z"
stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<span class="blind">검색</span>
</button>
</div>
</div>
</form>
<!-- Inquiry Table -->
<div class="board-table" th:if="${!inquiries.isEmpty()}">
<!-- Table Header -->
<div class="board-table-header">
<div class="header-cell" style="width: 80px;">NO</div>
<div class="header-cell" style="flex: 1; min-width: 200px;">제목</div>
<div class="header-cell" style="width: 120px;">등록일</div>
</div>
<!-- Table Body -->
<div class="board-table-body">
<div class="board-table-row" th:each="inquiry, status : ${inquiries}"
th:onclick="'location.href=\'' + @{/inquiry/detail(id=${inquiry.id})} + '\''"
style="cursor: pointer;">
<div class="row-cell row-cell--number" style="width: 80px;" data-label="NO"
th:text="${page.totalElements - (page.number * page.size) - status.index}">1</div>
<div class="row-cell row-cell--title" style="flex: 1; min-width: 200px;" data-label="제목">
<a th:href="@{/inquiry/detail(id=${inquiry.id})}" class="notice-title-link">
<span class="notice-number"
th:text="|[${page.totalElements - (page.number * page.size) - status.index}]|">[1]</span>
<span class="notice-type-badge notice-type-badge--maintenance"
th:if="${inquiry.inquiryStatus == 'PENDING'}">답변대기</span>
<span class="notice-type-badge notice-type-badge--fix"
th:if="${inquiry.inquiryStatus == 'RESPONDED'}">답변완료</span>
<span class="notice-type-badge notice-type-badge--incident"
th:if="${inquiry.inquiryStatus == 'CLOSED'}">종료</span>
<span th:text="${inquiry.inquirySubject}">질문 제목</span>
<span class="inquiry-comment-count"
th:if="${commentCounts != null and commentCounts.get(inquiry.id) != null and commentCounts.get(inquiry.id) > 0}"
th:text="|[${commentCounts.get(inquiry.id)}]|">[3]</span>
</a>
</div>
<div class="row-cell" style="width: 120px;" data-label="등록일"
th:text="${#temporals.format(inquiry.createdDate, 'yyyy.MM.dd')}">2025.01.01</div>
</div>
</div>
</div>
<!-- Empty State -->
<div class="user-empty-state" th:if="${inquiries.isEmpty()}">
<h3>조회된 Q&A가 없습니다</h3>
<p style="color: #64748b; margin-top: 8px;">검색 조건을 변경하거나 새로운 질문을 등록해주세요.</p>
</div>
<!-- Pagination -->
<div class="pagination" th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}">
</div>
<!-- New Inquiry Button -->
<div style="display: flex; justify-content: center; margin-top: 40px;">
<a th:href="@{/inquiry/new}" class="btn btn-primary"
style="width: 247px; height: 60px; font-size: 20px; font-weight: 700; border-radius: 10px; display: flex; align-items: center; justify-content: center; background: #2a69de; color: #fff; text-decoration: none;">
문의하기
</a>
</div>
</section>
</section>
</div> </div>
</div> </div>
</th:block>
<!-- Empty State -->
<div class="user-empty-state" th:if="${inquiries.isEmpty()}">
<h3>조회된 Q&A가 없습니다</h3>
<p>검색 조건을 변경하거나 새로운 질문을 등록해주세요.</p>
</div>
<!-- Pagination -->
<div class="pagination" th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}">
</div>
<!-- New Inquiry Button -->
<div class="inquiry-actions">
<a th:href="@{/inquiry/new}" class="btn btn-primary btn-lg">
<span>질문하기</span>
</a>
</div>
</section>
</th:block>
</body> </body>
<th:block layout:fragment="contentScript"> <th:block layout:fragment="contentScript">
<script th:inline="javascript"> <script th:inline="javascript">
$(document).ready(function () { $(document).ready(function () {
var successMsg = [[${success}]]; var successMsg = [[${ success }]];
if (successMsg) { if (successMsg) {
customPopups.showAlert(successMsg); customPopups.showAlert(successMsg);
} }
@@ -10,84 +10,107 @@
</div> </div>
</section> </section>
<section layout:fragment="contentFragment"> <th:block layout:fragment="contentFragment">
<div class="notice-detail-container"> <div class="signup-guide-v2">
<!-- Hero Section -->
<!-- Notice Header: Title and Date --> <section class="service-hero">
<div class="notice-detail-header"> <div class="service-hero__inner">
<h2 class="notice-detail-title"> <div class="service-hero__icon-wrapper">
<span th:if="${portalNotice.noticeType == '3'}" <img th:src="@{/img/keyimage/notice_img.svg}" alt="공지사항 아이콘"
style="display:inline-block;padding:2px 10px;margin-right:8px;border-radius:12px;font-size:14px;vertical-align:middle;background:#fde7e9;color:#c0152a;">장애</span> style="width: 100%; height: 100%; object-fit: contain;" />
<span th:if="${portalNotice.noticeType == '2'}" </div>
style="display:inline-block;padding:2px 10px;margin-right:8px;border-radius:12px;font-size:14px;vertical-align:middle;background:#fff4d6;color:#9a6700;">점검</span> <div class="service-hero__content">
<span th:text="${portalNotice.noticeSubject}">개인정보처리 방침 정정 공지</span> <div class="service-hero__badge">
</h2> <span class="service-hero__badge-dot"></span>
<span class="notice-detail-date" th:text="${#temporals.format(portalNotice.createdDate, 'yyyy.MM.dd')}">2025.11.01</span> <span class="service-hero__badge-text">고객지원</span>
</div> </div>
<h1 class="service-hero__title">공지사항</h1>
<!-- 장애/점검 정보 영역 --> <p class="service-hero__desc">DJBank 오픈 API 포털의 주요 안내 및 업데이트 소식을 전해드립니다.<br>원활한 서비스 연동을 위해 변경 사항을 주기적으로 확인해 주시기 바랍니다.</p>
<div class="notice-detail-incident-info" th:if="${portalNotice.incidentOrMaintenance}"
style="margin:16px 0;padding:16px;background:#f7f8fa;border:1px solid #e3e6eb;border-radius:8px;">
<table style="width:100%;border-collapse:collapse;">
<colgroup>
<col style="width:120px;"><col><col style="width:120px;"><col>
</colgroup>
<tr>
<th style="text-align:left;padding:6px 8px;">시작</th>
<td style="padding:6px 8px;"
th:text="${portalNotice.startedAt != null ? #temporals.format(portalNotice.startedAt, 'yyyy.MM.dd HH:mm') : '-'}">-</td>
<th style="text-align:left;padding:6px 8px;">종료</th>
<td style="padding:6px 8px;"
th:text="${portalNotice.endAt != null ? #temporals.format(portalNotice.endAt, 'yyyy.MM.dd HH:mm') : '진행중'}">-</td>
</tr>
<tr th:if="${portalNotice.incidentType}">
<th style="text-align:left;padding:6px 8px;">상태</th>
<td colspan="3" style="padding:6px 8px;" th:text="${portalNotice.state != null ? portalNotice.state : '-'}">-</td>
</tr>
<tr th:if="${portalNotice.affectedApis != null and !portalNotice.affectedApis.isEmpty()}">
<th style="text-align:left;padding:6px 8px;vertical-align:top;">영향 API</th>
<td colspan="3" style="padding:6px 8px;">
<ul style="margin:0;padding-left:18px;">
<li th:each="api : ${portalNotice.affectedApis}">
<strong th:text="${api.apiId}">API_ID</strong>
<span th:if="${api.apiName != null and !api.apiName.isEmpty()}"
th:text="| - ${api.apiName}|"></span>
</li>
</ul>
</td>
</tr>
</table>
</div>
<!-- Attachment Section -->
<div class="notice-detail-attachment" th:if="${!#strings.isEmpty(portalNotice.fileId)}">
<div class="attachment-list" th:with="fileInfo=${@fileService.findById(portalNotice.fileId)}">
<div class="attachment-item" th:each="fileDetail, status : ${fileInfo.getFileDetails()}">
<a th:href="'javascript:fn_downloadFile(\'' + ${fileDetail.fileId} + '\',\''+ ${fileDetail.fileSn} +'\')'" class="notice-attachment-link">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 15.75C18 17.4833 17.3917 18.9583 16.175 20.175C14.9583 21.3917 13.4833 22 11.75 22C10.0167 22 8.54167 21.3917 7.325 20.175C6.10833 18.9583 5.5 17.4833 5.5 15.75V6.5C5.5 5.25 5.9375 4.1875 6.8125 3.3125C7.6875 2.4375 8.75 2 10 2C11.25 2 12.3125 2.4375 13.1875 3.3125C14.0625 4.1875 14.5 5.25 14.5 6.5V15.25C14.5 16.0167 14.2333 16.6667 13.7 17.2C13.1667 17.7333 12.5167 18 11.75 18C10.9833 18 10.3333 17.7333 9.8 17.2C9.26667 16.6667 9 16.0167 9 15.25V6H11V15.25C11 15.4667 11.0708 15.6458 11.2125 15.7875C11.3542 15.9292 11.5333 16 11.75 16C11.9667 16 12.1458 15.9292 12.2875 15.7875C12.4292 15.6458 12.5 15.4667 12.5 15.25V6.5C12.4833 5.8 12.2375 5.20833 11.7625 4.725C11.2875 4.24167 10.7 4 10 4C9.3 4 8.70833 4.24167 8.225 4.725C7.74167 5.20833 7.5 5.8 7.5 6.5V15.75C7.48333 16.9333 7.89167 17.9375 8.725 18.7625C9.55833 19.5875 10.5667 20 11.75 20C12.9167 20 13.9083 19.5875 14.725 18.7625C15.5417 17.9375 15.9667 16.9333 16 15.75V6H18V15.75Z" fill="currentColor"/>
</svg>
<span class="attachment-label">첨부파일</span>
<span class="attachment-filename">[[${fileDetail.originalFileName}]].[[${fileDetail.fileExtension}]]</span>
</a>
</div> </div>
</div> </div>
</div> </section>
<!-- Content Section --> <!-- Main Content -->
<div class="notice-detail-content"> <div class="service-main">
<div id="noticeDetail" class="notice-content-body editor-content" th:utext="${portalNotice.noticeDetail}"> <!-- Sidebar -->
공지사항 내용이 여기에 표시됩니다. <th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('notice')}"></th:block>
</div>
</div>
<!-- Action Buttons --> <!-- Content -->
<div class="notice-detail-actions"> <section class="service-content">
<button type="button" class="btn-notice-list" th:onclick="|location.href='@{/portalnotice}'|">목록</button> <div class="notice-detail-container" style="padding: 0; min-height: auto; margin: 0; background: transparent; box-shadow: none;">
</div>
<!-- Notice Header: Title and Date -->
<div class="notice-detail-header">
<p class="notice-detail-date" th:text="${#temporals.format(portalNotice.createdDate, 'yyyy.MM.dd')}">2026.06.24</p>
<div class="notice-detail-title-wrapper">
<span class="notice-type-badge notice-type-badge--incident" th:if="${portalNotice.noticeType == '3'}">장애</span>
<span class="notice-type-badge notice-type-badge--maintenance" th:if="${portalNotice.noticeType == '2'}">점검</span>
<h2 class="notice-detail-title" th:text="${portalNotice.noticeSubject}">공지사항 디테일 입니다</h2>
</div>
</div>
<!-- 장애/점검 정보 영역 -->
<div class="notice-detail-incident-info" th:if="${portalNotice.incidentOrMaintenance}">
<table class="detail-table">
<colgroup>
<col style="width: 160px;"><col><col style="width: 160px;"><col>
</colgroup>
<tbody>
<tr>
<th>시작</th>
<td th:text="${portalNotice.startedAt != null ? #temporals.format(portalNotice.startedAt, 'yyyy-MM-dd HH:mm') : '-'}">-</td>
<th></th>
<td th:text="${portalNotice.endAt != null ? #temporals.format(portalNotice.endAt, 'yyyy-MM-dd HH:mm') : '진행중'}">-</td>
</tr>
<tr>
<th>상태</th>
<td th:text="${portalNotice.state != null ? portalNotice.state : '-'}">-</td>
<th>영향 API</th>
<td>
<th:block th:if="${portalNotice.affectedApis != null and !portalNotice.affectedApis.isEmpty()}">
<span th:each="api, iterStat : ${portalNotice.affectedApis}">
<strong th:text="${api.apiId}">API_ID</strong><span th:if="${api.apiName != null and !api.apiName.isEmpty()}" th:text="| - ${api.apiName}|"></span><th:block th:if="${!iterStat.last}">, </th:block>
</span>
</th:block>
<th:block th:unless="${portalNotice.affectedApis != null and !portalNotice.affectedApis.isEmpty()}">-</th:block>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Attachment Section -->
<div class="notice-detail-attachment" th:if="${!#strings.isEmpty(portalNotice.fileId)}">
<div class="attachment-list" th:with="fileInfo=${@fileService.findById(portalNotice.fileId)}">
<div class="attachment-item" th:each="fileDetail, status : ${fileInfo.getFileDetails()}">
<a th:href="'javascript:fn_downloadFile(\'' + ${fileDetail.fileId} + '\',\''+ ${fileDetail.fileSn} +'\')'" class="notice-attachment-link">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 15.75C18 17.4833 17.3917 18.9583 16.175 20.175C14.9583 21.3917 13.4833 22 11.75 22C10.0167 22 8.54167 21.3917 7.325 20.175C6.10833 18.9583 5.5 17.4833 5.5 15.75V6.5C5.5 5.25 5.9375 4.1875 6.8125 3.3125C7.6875 2.4375 8.75 2 10 2C11.25 2 12.3125 2.4375 13.1875 3.3125C14.0625 4.1875 14.5 5.25 14.5 6.5V15.25C14.5 16.0167 14.2333 16.6667 13.7 17.2C13.1667 17.7333 12.5167 18 11.75 18C10.9833 18 10.3333 17.7333 9.8 17.2C9.26667 16.6667 9 16.0167 9 15.25V6H11V15.25C11 15.4667 11.0708 15.6458 11.2125 15.7875C11.3542 15.9292 11.5333 16 11.75 16C11.9667 16 12.1458 15.9292 12.2875 15.7875C12.4292 15.6458 12.5 15.4667 12.5 15.25V6.5C12.4833 5.8 12.2375 5.20833 11.7625 4.725C11.2875 4.24167 10.7 4 10 4C9.3 4 8.70833 4.24167 8.225 4.725C7.74167 5.20833 7.5 5.8 7.5 6.5V15.75C7.48333 16.9333 7.89167 17.9375 8.725 18.7625C9.55833 19.5875 10.5667 20 11.75 20C12.9167 20 13.9083 19.5875 14.725 18.7625C15.5417 17.9375 15.9667 16.9333 16 15.75V6H18V15.75Z" fill="currentColor"/>
</svg>
<span class="attachment-label">첨부파일</span>
<span class="attachment-filename">[[${fileDetail.originalFileName}]].[[${fileDetail.fileExtension}]]</span>
</a>
</div>
</div>
</div>
<!-- Content Section -->
<div class="notice-detail-content">
<div id="noticeDetail" class="notice-content-body editor-content" th:utext="${portalNotice.noticeDetail}">
공지사항 내용이 여기에 표시됩니다.
</div>
</div>
<!-- Action Buttons -->
<div class="notice-detail-actions">
<button type="button" class="btn-notice-list" th:onclick="|location.href='@{/portalnotice}'|">목록</button>
</div>
</div>
</section>
</div>
</div> </div>
</section> </th:block>
</body> </body>
@@ -1,97 +1,123 @@
<!doctype html> <!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" <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_title_layout}"> xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/djbank_title_layout}">
<body> <body>
<section layout:fragment="title"> <section layout:fragment="title">
<div class="page-title-banner"> <div class="page-title-banner">
<img th:src="@{/img/img_title_bg.png}" class="title-image" alt="타이틀 배경"> <img th:src="@{/img/img_title_bg.png}" class="title-image" alt="타이틀 배경">
<h1>공지사항</h1> <h1>공지사항</h1>
</div>
</section>
<th:block layout:fragment="contentFragment">
<section class="user-management-container notice-list-container">
<!-- Search and Filter Controls -->
<form name="noticeForm" th:action="@{/portalnotice}" method="get" onsubmit="return false;">
<input type="hidden" name="page" value="1">
<div class="table-controls">
<h2 class="total-count">
<strong th:text="${page.totalElements}">0</strong>
</h2>
<div class="search-field">
<input type="text"
name="searchSubjectOrDetail"
th:value="${search.searchSubjectOrDetail}"
placeholder="제목/내용 검색"
onkeypress="if(event.keyCode === 13) { fn_search_notice(); return false; }">
<button type="button" class="search-field-btn" onclick="fn_search_notice()">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.5 17.5L13.875 13.875M15.8333 9.16667C15.8333 12.8486 12.8486 15.8333 9.16667 15.8333C5.48477 15.8333 2.5 12.8486 2.5 9.16667C2.5 5.48477 5.48477 2.5 9.16667 2.5C12.8486 2.5 15.8333 5.48477 15.8333 9.16667Z" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span class="blind">검색</span>
</button>
</div>
</div>
</form>
<!-- Notice Table -->
<div class="list-table" th:if="${!portalNotices.isEmpty()}">
<!-- Table Header -->
<div class="list-table-header">
<div class="header-cell" style="width: 80px;">NO</div>
<div class="header-cell" style="flex: 1; min-width: 200px;">제목</div>
<div class="header-cell" style="width: 120px;">등록일</div>
</div>
<!-- Table Body -->
<div class="list-table-body">
<div class="list-table-row"
th:each="notice, status : ${portalNotices}"
th:onclick="'location.href=\'' + @{/portalnotice/detail(id=${notice.id})} + '\''"
style="cursor: pointer;">
<div class="row-cell row-cell--number" style="width: 80px;" data-label="NO"
th:text="${page.totalElements - (page.number * page.size) - status.index}">1</div>
<div class="row-cell row-cell--title" style="flex: 1; min-width: 200px;" data-label="제목">
<a th:href="@{/portalnotice/detail(id=${notice.id})}" class="notice-title-link">
<span class="notice-number" th:text="|[${page.totalElements - (page.number * page.size) - status.index}]|">[1]</span>
<span class="notice-fix-badge"
th:if="${notice.fixYn == 'Y'}"
style="display:inline-block;padding:2px 8px;margin-right:6px;border-radius:10px;font-size:12px;background:#e5f0ff;color:#0a4ea3;font-weight:600;">고정</span>
<span class="notice-type-badge notice-type-badge--incident"
th:if="${notice.noticeType == '3'}"
style="display:inline-block;padding:2px 8px;margin-right:6px;border-radius:10px;font-size:12px;background:#fde7e9;color:#c0152a;">장애</span>
<span class="notice-type-badge notice-type-badge--maintenance"
th:if="${notice.noticeType == '2'}"
style="display:inline-block;padding:2px 8px;margin-right:6px;border-radius:10px;font-size:12px;background:#fff4d6;color:#9a6700;">점검</span>
<span th:text="${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 d="M18 15.75C18 17.4833 17.3917 18.9583 16.175 20.175C14.9583 21.3917 13.4833 22 11.75 22C10.0167 22 8.54167 21.3917 7.325 20.175C6.10833 18.9583 5.5 17.4833 5.5 15.75V6.5C5.5 5.25 5.9375 4.1875 6.8125 3.3125C7.6875 2.4375 8.75 2 10 2C11.25 2 12.3125 2.4375 13.1875 3.3125C14.0625 4.1875 14.5 5.25 14.5 6.5V15.25C14.5 16.0167 14.2333 16.6667 13.7 17.2C13.1667 17.7333 12.5167 18 11.75 18C10.9833 18 10.3333 17.7333 9.8 17.2C9.26667 16.6667 9 16.0167 9 15.25V6H11V15.25C11 15.4667 11.0708 15.6458 11.2125 15.7875C11.3542 15.9292 11.5333 16 11.75 16C11.9667 16 12.1458 15.9292 12.2875 15.7875C12.4292 15.6458 12.5 15.4667 12.5 15.25V6.5C12.4833 5.8 12.2375 5.20833 11.7625 4.725C11.2875 4.24167 10.7 4 10 4C9.3 4 8.70833 4.24167 8.225 4.725C7.74167 5.20833 7.5 5.8 7.5 6.5V15.75C7.48333 16.9333 7.89167 17.9375 8.725 18.7625C9.55833 19.5875 10.5667 20 11.75 20C12.9167 20 13.9083 19.5875 14.725 18.7625C15.5417 17.9375 15.9667 16.9333 16 15.75V6H18V15.75Z" fill="currentColor"/>
</svg>
</span>
</a>
</div>
<div class="row-cell" style="width: 120px;" data-label="등록일"
th:text="${#temporals.format(notice.createdDate, 'yyyy.MM.dd')}">2025.01.01</div>
</div>
</div>
</div> </div>
<!-- Empty State -->
<div class="user-empty-state" th:if="${portalNotices.isEmpty()}">
<h3>조회된 공지사항이 없습니다</h3>
</div>
<!-- Pagination -->
<div class="pagination" th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}">
</div>
</section> </section>
</th:block>
<th:block layout:fragment="contentFragment">
<div class="signup-guide-v2">
<!-- Hero Section -->
<section class="service-hero">
<div class="service-hero__inner">
<div class="service-hero__icon-wrapper">
<img th:src="@{/img/keyimage/notice_img.svg}" alt="공지사항 아이콘"
style="width: 100%; height: 100%; object-fit: contain;" />
</div>
<div class="service-hero__content">
<div class="service-hero__badge">
<span class="service-hero__badge-dot"></span>
<span class="service-hero__badge-text">고객지원</span>
</div>
<h1 class="service-hero__title">공지사항</h1>
<p class="service-hero__desc">DJBank 오픈 API 포털의 주요 안내 및 업데이트 소식을 전해드립니다.<br>원활한 서비스 연동을 위해 변경 사항을 주기적으로 확인해
주시기 바랍니다</p>
</div>
</div>
</section>
<!-- Main Content -->
<div class="service-main">
<!-- Sidebar -->
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('notice')}"></th:block>
<!-- Content -->
<section class="service-content">
<section class="user-management-container notice-list-container"
style="padding: 0; min-height: auto; margin: 0; background: transparent; box-shadow: none;">
<!-- Search and Filter Controls -->
<form name="noticeForm" th:action="@{/portalnotice}" method="get" onsubmit="return false;">
<input type="hidden" name="page" value="1">
<div class="table-controls">
<h2 class="total-count">
<strong th:text="${page.totalElements}">0</strong>
</h2>
<div class="search-field">
<input type="text" name="searchSubjectOrDetail" th:value="${search.searchSubjectOrDetail}"
placeholder="제목/내용 검색" onkeypress="if(event.keyCode === 13) { fn_search_notice(); return false; }">
<button type="button" class="search-field-btn" onclick="fn_search_notice()">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M17.5 17.5L13.875 13.875M15.8333 9.16667C15.8333 12.8486 12.8486 15.8333 9.16667 15.8333C5.48477 15.8333 2.5 12.8486 2.5 9.16667C2.5 5.48477 5.48477 2.5 9.16667 2.5C12.8486 2.5 15.8333 5.48477 15.8333 9.16667Z"
stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<span class="blind">검색</span>
</button>
</div>
</div>
</form>
<!-- Notice Table -->
<div class="board-table" th:if="${!portalNotices.isEmpty()}">
<!-- Table Header -->
<div class="board-table-header">
<div class="header-cell" style="width: 80px;">NO</div>
<div class="header-cell" style="flex: 1; min-width: 200px;">제목</div>
<div class="header-cell" style="width: 120px;">등록일</div>
</div>
<!-- Table Body -->
<div class="board-table-body">
<div class="board-table-row" th:each="notice, status : ${portalNotices}"
th:onclick="'location.href=\'' + @{/portalnotice/detail(id=${notice.id})} + '\''"
style="cursor: pointer;">
<div class="row-cell row-cell--number" style="width: 80px;" data-label="NO"
th:text="${page.totalElements - (page.number * page.size) - status.index}">1</div>
<div class="row-cell row-cell--title" style="flex: 1; min-width: 200px;" data-label="제목">
<a th:href="@{/portalnotice/detail(id=${notice.id})}" class="notice-title-link">
<span class="notice-number"
th:text="|[${page.totalElements - (page.number * page.size) - status.index}]|">[1]</span>
<span class="notice-type-badge notice-type-badge--fix" th:if="${notice.fixYn == 'Y'}">고정</span>
<span class="notice-type-badge notice-type-badge--incident" th:if="${notice.noticeType == '3'}">장애</span>
<span class="notice-type-badge notice-type-badge--maintenance" th:if="${notice.noticeType == '2'}">점검</span>
<span th:text="${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
d="M18 15.75C18 17.4833 17.3917 18.9583 16.175 20.175C14.9583 21.3917 13.4833 22 11.75 22C10.0167 22 8.54167 21.3917 7.325 20.175C6.10833 18.9583 5.5 17.4833 5.5 15.75V6.5C5.5 5.25 5.9375 4.1875 6.8125 3.3125C7.6875 2.4375 8.75 2 10 2C11.25 2 12.3125 2.4375 13.1875 3.3125C14.0625 4.1875 14.5 5.25 14.5 6.5V15.25C14.5 16.0167 14.2333 16.6667 13.7 17.2C13.1667 17.7333 12.5167 18 11.75 18C10.9833 18 10.3333 17.7333 9.8 17.2C9.26667 16.6667 9 16.0167 9 15.25V6H11V15.25C11 15.4667 11.0708 15.6458 11.2125 15.7875C11.3542 15.9292 11.5333 16 11.75 16C11.9667 16 12.1458 15.9292 12.2875 15.7875C12.4292 15.6458 12.5 15.4667 12.5 15.25V6.5C12.4833 5.8 12.2375 5.20833 11.7625 4.725C11.2875 4.24167 10.7 4 10 4C9.3 4 8.70833 4.24167 8.225 4.725C7.74167 5.20833 7.5 5.8 7.5 6.5V15.75C7.48333 16.9333 7.89167 17.9375 8.725 18.7625C9.55833 19.5875 10.5667 20 11.75 20C12.9167 20 13.9083 19.5875 14.725 18.7625C15.5417 17.9375 15.9667 16.9333 16 15.75V6H18V15.75Z"
fill="currentColor" />
</svg>
</span>
</a>
</div>
<div class="row-cell" style="width: 120px;" data-label="등록일"
th:text="${#temporals.format(notice.createdDate, 'yyyy.MM.dd')}">2025.01.01</div>
</div>
</div>
</div>
<!-- Empty State -->
<div class="user-empty-state" th:if="${portalNotices.isEmpty()}">
<h3>조회된 공지사항이 없습니다</h3>
</div>
<!-- Pagination -->
<div class="pagination" th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}">
</div>
</section>
</section>
</div>
</div>
</th:block>
</body> </body>
@@ -111,9 +137,10 @@
document.noticeForm.submit(); document.noticeForm.submit();
} }
$(function() { $(function () {
fn_init(); fn_init();
}); });
</script> </script>
</th:block> </th:block>
</html> </html>
@@ -1,132 +1,96 @@
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" <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_title_layout}"> xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/djbank_title_layout}">
<body> <body>
<section layout:fragment="title"> <section layout:fragment="title">
<div class="page-title-banner"> <div class="page-title-banner">
<img th:src="@{/img/img_title_bg.png}" class="title-image" alt="타이틀 배경"> <img th:src="@{/img/img_title_bg.png}" class="title-image" alt="타이틀 배경">
<h1>피드백/개선요청</h1> <h1>피드백/개선요청</h1>
</div>
</section>
<th:block layout:fragment="contentFragment">
<section class="inquiry-form-container">
<!-- Title Bar -->
<div class="register-title-bar">
<h2 class="register-title">피드백/개선요청</h2>
<span class="register-subtitle">DJBank은 온라인 비즈니스 혁신을 위한 피드백/개선요청을 환영합니다.</span>
</div> </div>
<!-- Form Content -->
<div class="register-form-container">
<form name="partnershipForm" id="partnershipForm" th:action="@{/partnership}" th:object="${partnership}"
enctype="multipart/form-data" method="post" class="register-form">
<!-- 제목 Field -->
<div class="form-row">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">제목</span>
<span class="required-badge">필수</span>
</div>
<div class="form-field-wrapper">
<input type="text"
id="bizSubject"
th:field="*{bizSubject}"
class="form-input"
placeholder="제안하고자 하는 제목을 입력해 주세요"
required
maxlength="200">
</div>
</div>
<!-- 사업 내용 Field -->
<div class="form-row">
<div class="form-label-wrapper">
<span class="form-label-text">내용</span>
<span class="required-badge">필수</span>
</div>
<div class="form-field-wrapper">
<textarea id="bizDetail"
th:field="*{bizDetail}"
class="form-textarea"
rows="10"
placeholder="제안하고자 하시는 내용을 상세히 입력해 주세요"
required
maxlength="4000"></textarea>
<small class="char-counter-wrapper">
<span class="char-counter">0 / 4000</span>
</small>
</div>
</div>
<!-- File Upload Field -->
<!--/* 260520 - DJB 에서는 첨부 파일 허용안함. */-->
<!--<div class="form-row">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">첨부파일</span>
</div>
<div class="form-field-wrapper">
<div class="file-upload-inline">
<div class="file-input-display" id="fileInputDisplay">
<span class="file-display-text" id="fileDisplayText">선택된 파일이 없습니다</span>
<button type="button" class="btn-remove-file-inline" id="btnRemoveFile" title="파일 삭제" style="display: none;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<button type="button" class="btn-file-attach" onclick="document.getElementById('file').click()">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 15.75C18 17.4833 17.3917 18.9583 16.175 20.175C14.9583 21.3917 13.4833 22 11.75 22C10.0167 22 8.54167 21.3917 7.325 20.175C6.10833 18.9583 5.5 17.4833 5.5 15.75V6.5C5.5 5.25 5.9375 4.1875 6.8125 3.3125C7.6875 2.4375 8.75 2 10 2C11.25 2 12.3125 2.4375 13.1875 3.3125C14.0625 4.1875 14.5 5.25 14.5 6.5V15.25C14.5 16.0167 14.2333 16.6667 13.7 17.2C13.1667 17.7333 12.5167 18 11.75 18C10.9833 18 10.3333 17.7333 9.8 17.2C9.26667 16.6667 9 16.0167 9 15.25V6H11V15.25C11 15.4667 11.0708 15.6458 11.2125 15.7875C11.3542 15.9292 11.5333 16 11.75 16C11.9667 16 12.1458 15.9292 12.2875 15.7875C12.4292 15.6458 12.5 15.4667 12.5 15.25V6.5C12.4833 5.8 12.2375 5.20833 11.7625 4.725C11.2875 4.24167 10.7 4 10 4C9.3 4 8.70833 4.24167 8.225 4.725C7.74167 5.20833 7.5 5.8 7.5 6.5V15.75C7.48333 16.9333 7.89167 17.9375 8.725 18.7625C9.55833 19.5875 10.5667 20 11.75 20C12.9167 20 13.9083 19.5875 14.725 18.7625C15.5417 17.9375 15.9667 16.9333 16 15.75V6H18V15.75Z" fill="currentColor"/>
</svg>
파일첨부
</button>
<input type="file"
id="file"
name="files"
th:accept="${isInternalUser ? '*' : '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.hwp,.gif,.jpg,.jpeg,.png'}"
style="display: none;">
</div>
<div class="form-help-text">
<p><i class="fas fa-check-circle"></i>
<span th:text="'첨부파일은 ' + ${@portalProperties.file.maxSize} + ' 이내로 등록해 주세요.'">첨부파일은 8MB 이내로 등록해 주세요.</span>
</p>
<p>
<i class="fas fa-check-circle"></i>
문서파일과 이미지 파일만 등록 가능합니다. (pdf, doc, docx, xls, xlsx, ppt, pptx, hwp, gif, jpg, png)
</p>
</div>
</div>
</div>-->
</form>
</div>
<!-- Form Actions -->
<div class="form-actions">
<button type="button" class="btn btn-submit btn-secondary" th:onclick="|location.href='@{/}'|">취소</button>
<button type="button" class="btn btn-submit btn-primary btn-submit-form">
<span>작성 완료</span>
</button>
</div>
</section> </section>
</th:block>
<th:block layout:fragment="contentFragment">
<div class="signup-guide-v2">
<!-- Hero Section -->
<section class="service-hero">
<div class="service-hero__inner">
<div class="service-hero__icon-wrapper">
<img th:src="@{/img/keyimage/feedback_img.svg}" alt="피드백 아이콘"
style="width: 100%; height: 100%; object-fit: contain;" />
</div>
<div class="service-hero__content">
<div class="service-hero__badge">
<span class="service-hero__badge-dot"></span>
<span class="service-hero__badge-text">피드백/개선요청</span>
</div>
<h1 class="service-hero__title">피드백/개선요청</h1>
<p class="service-hero__desc">DJBank 오픈 API 이용 중 발생한 피드백이나 개선요청을 보내주세요<br>작성해주신 내용은 담당자 확인 후 적극 반영하겠습니다.</p>
</div>
</div>
</section>
<!-- Main Content -->
<div class="service-main">
<!-- Sidebar -->
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('feedback')}"></th:block>
<!-- Content -->
<section class="service-content">
<div class="djb-board-write-container">
<!-- Title Bar -->
<div class="djb-board-write-header">
<h2 class="title">피드백 / 개선 요청</h2>
<p class="subtitle">DJ Bank은 온라인 비즈니스 혁신을 위한 피드백/개선요청을 환영합니다.</p>
</div>
<!-- Form Content -->
<form name="partnershipForm" id="partnershipForm" th:action="@{/partnership}" th:object="${partnership}"
enctype="multipart/form-data" method="post" class="djb-board-form">
<!-- 제목 Field -->
<div class="form-group">
<label class="djb-label" for="bizSubject">제목 <span class="required">*</span></label>
<input type="text" id="bizSubject" th:field="*{bizSubject}" class="djb-input"
placeholder="제안하고자 하는 제목을 입력해주세요" required maxlength="200">
</div>
<!-- 내용 Field -->
<div class="form-group">
<label class="djb-label" for="bizDetail">내용 <span class="required">*</span></label>
<textarea id="bizDetail" th:field="*{bizDetail}" class="djb-textarea"
placeholder="제안하고자 하는 내용을 상세히 입력해주세요" required maxlength="4000"></textarea>
<div class="djb-char-counter-wrapper">
<span class="char-counter">0 / 4000</span>
</div>
</div>
</form>
<!-- Form Actions -->
<div class="form-actions">
<button type="button" class="btn-cancel" th:onclick="|location.href='@{/}'|">취소</button>
<button type="button" class="btn-submit btn-submit-form">작성완료</button>
</div>
</div>
</section>
</div>
</div>
</th:block>
</body> </body>
<th:block layout:fragment="contentScript"> <th:block layout:fragment="contentScript">
<script th:if="${error}" th:inline="javascript"> <script th:if="${error}" th:inline="javascript">
$(document).ready(function () { $(document).ready(function () {
const errorMsg = [[${error}]]; const errorMsg = [[${ error }]];
customPopups.showAlert(errorMsg); customPopups.showAlert(errorMsg);
}); });
</script> </script>
<script th:if="${success}" th:inline="javascript"> <script th:if="${success}" th:inline="javascript">
$(document).ready(function () { $(document).ready(function () {
customPopups.showAlert([[${success}]]); customPopups.showAlert([[${ success }]]);
}); });
</script> </script>
<script th:inline="javascript"> <script th:inline="javascript">
@@ -146,7 +110,7 @@
charCounter.textContent = currentLength + ' / ' + maxLength; charCounter.textContent = currentLength + ' / ' + maxLength;
// Character counter for textarea // Character counter for textarea
bizDetailTextarea.addEventListener('input', function() { bizDetailTextarea.addEventListener('input', function () {
const currentLength = this.value.length; const currentLength = this.value.length;
const maxLength = this.getAttribute('maxlength'); const maxLength = this.getAttribute('maxlength');
charCounter.textContent = currentLength + ' / ' + maxLength; charCounter.textContent = currentLength + ' / ' + maxLength;
@@ -183,41 +147,50 @@
form.submit(); form.submit();
}); });
fileInput.addEventListener('change', function () { if (fileInput) {
const file = fileInput.files[0]; fileInput.addEventListener('change', function () {
const file = fileInput.files[0];
if (file) { if (file) {
/*[# th:if="${!isInternalUser}"]*/ /*[# th:if="${!isInternalUser}"]*/
const allowedExtensions = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.hwp', '.gif', '.jpg', '.jpeg', '.png']; const allowedExtensions = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.hwp', '.gif', '.jpg', '.jpeg', '.png'];
const fileExt = '.' + file.name.split('.').pop().toLowerCase(); const fileExt = '.' + file.name.split('.').pop().toLowerCase();
if (!allowedExtensions.includes(fileExt)) { if (!allowedExtensions.includes(fileExt)) {
customPopups.showAlert("허용된 파일 형식이 아닙니다.\n문서파일(pdf, doc, docx, xls, xlsx, ppt, pptx, hwp)과 이미지 파일(gif, jpg, png)만 등록 가능합니다."); customPopups.showAlert("허용된 파일 형식이 아닙니다.\n문서파일(pdf, doc, docx, xls, xlsx, ppt, pptx, hwp)과 이미지 파일(gif, jpg, png)만 등록 가능합니다.");
fileInput.value = ''; fileInput.value = '';
return; return;
}
/*[/]*/
if (fileDisplayText && fileInputDisplay && btnRemoveFile) {
fileDisplayText.textContent = file.name;
fileDisplayText.classList.add('has-file');
fileInputDisplay.classList.add('has-file');
btnRemoveFile.style.display = 'flex';
}
} }
/*[/]*/ });
}
fileDisplayText.textContent = file.name;
fileDisplayText.classList.add('has-file');
fileInputDisplay.classList.add('has-file');
btnRemoveFile.style.display = 'flex';
}
});
// File remove handler // File remove handler
btnRemoveFile.addEventListener('click', function (e) { if (btnRemoveFile) {
e.stopPropagation(); btnRemoveFile.addEventListener('click', function (e) {
fileInput.value = ''; e.stopPropagation();
fileDisplayText.textContent = '선택된 파일이 없습니다'; if (fileInput) fileInput.value = '';
fileDisplayText.classList.remove('has-file'); if (fileDisplayText) {
fileInputDisplay.classList.remove('has-file'); fileDisplayText.textContent = '선택된 파일이 없습니다';
btnRemoveFile.style.display = 'none'; fileDisplayText.classList.remove('has-file');
}); }
if (fileInputDisplay) fileInputDisplay.classList.remove('has-file');
this.style.display = 'none';
});
}
// Focus on subject field // Focus on subject field
document.getElementById('bizSubject').focus(); document.getElementById('bizSubject').focus();
}); });
</script> </script>
</th:block> </th:block>
</html> </html>
File diff suppressed because one or more lines are too long
@@ -1,153 +1,217 @@
<!doctype html> <!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" <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_title_layout}"> xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/djbank_title_layout}">
<body> <body>
<th:block layout:fragment="contentFragment"> <th:block layout:fragment="contentFragment">
<div class="signup-guide"> <div class="signup-guide-v2">
<!-- Header Section --> <!-- Hero Section -->
<div class="signup-guide__header"> <section class="service-hero">
<h1 class="signup-guide__title">회원 가입 안내</h1> <div class="service-hero__inner">
<div class="signup-guide__underline"></div> <div class="service-hero__icon-wrapper">
<div class="signup-guide__desc"> <img th:src="@{/img/keyimage/signUp_img.svg}" alt="회원가입 소개 3D 아이콘"
<p>DJBank API 개발자 포털에 방문해 주셔서 감사합니다.</p> style="width: 100%; height: 100%; object-fit: contain;" />
<p>DJBank API 사용을 위해서는 다음과 같은 이용절차로 진행하여야 합니다.</p> </div>
<div class="service-hero__content">
<div class="service-hero__badge">
<span class="service-hero__badge-dot"></span>
<span class="service-hero__badge-text">회원가입 소개</span>
</div>
<h1 class="service-hero__title">회원 가입 안내</h1>
<p class="service-hero__desc">DJ Bank API 개발자 포털에 방문해 주셔서 감사합니다.<br>DJBank API 사용을 위해서는 다음과 같은
이용절차로 진행하여야 합니다.</p>
</div>
</div>
</section>
<!-- Main Content -->
<div class="service-main">
<!-- Sidebar -->
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('guide')}"></th:block>
<!-- Content -->
<section class="service-content">
<!-- Info Box -->
<div class="signup-info-box">
<div class="signup-info-box__icon">
<img th:src="@{/img/icon/icon_check_green.png}" alt="체크 아이콘" />
</div>
<ul class="signup-info-list">
<li>회원은 법인, 개인(개발자)분으로 구분되며, 개발자 포털에서 온라인으로 회원 가입 신청을 합니다.</li>
<li>포털 사용은 운영 담당자의 승인 후 가능합니다.</li>
<li>법인 관리자는 회원 승인 이후, 포털을 실제 사용할 직원(개발자)을 추가 등록해 주시기 바랍니다.</li>
</ul>
</div>
<!-- Vertical Timeline Steps -->
<div class="signup-timeline">
<!-- Step 1 -->
<div class="signup-step">
<div class="signup-step__icon-box one">
<!-- <svg width="40" height="40" viewBox="0 0 32 32" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M14 20C14 19.4477 14.4477 19 15 19H17C17.5523 19 18 19.4477 18 20V24C18 24.5523 17.5523 25 17 25H15C14.4477 25 14 24.5523 14 24V20Z"
stroke="#0049B4" stroke-width="1.6" stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M11 13C11 11.3431 12.3431 10 14 10C15.6569 10 17 11.3431 17 13C17 14.6569 15.6569 16 14 16C12.3431 16 11 14.6569 11 13Z"
stroke="#0049B4" stroke-width="1.6" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M20 21V20C20 18.8954 20.8954 18 22 18C23.1046 18 24 18.8954 24 20V21"
stroke="#0049B4" stroke-width="1.6" stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M19 14C19 12.8954 19.8954 12 21 12C22.1046 12 23 12.8954 23 14C23 15.1046 22.1046 16 21 16C19.8954 16 19 15.1046 19 14Z"
stroke="#0049B4" stroke-width="1.6" stroke-linecap="round"
stroke-linejoin="round" />
</svg> -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-person-add" viewBox="0 0 16 16">
<path
d="M12.5 16a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7m.5-5v1h1a.5.5 0 0 1 0 1h-1v1a.5.5 0 0 1-1 0v-1h-1a.5.5 0 0 1 0-1h1v-1a.5.5 0 0 1 1 0m-2-6a3 3 0 1 1-6 0 3 3 0 0 1 6 0M8 7a2 2 0 1 0 0-4 2 2 0 0 0 0 4" />
<path
d="M8.256 14a4.5 4.5 0 0 1-.229-1.004H3c.001-.246.154-.986.832-1.664C4.484 10.68 5.711 10 8 10q.39 0 .74.025c.226-.341.496-.65.804-.918Q8.844 9.002 8 9c-5 0-6 3-6 4s1 1 1 1z" />
</svg>
</div>
<div class="signup-step__card">
<div class="signup-step__card-num">STEP 01</div>
<div class="signup-step__card-title">회원가입</div>
</div>
</div>
<!-- Step 2 -->
<div class="signup-step">
<div class="signup-step__icon-box two">
<!-- <svg width="40" height="40" viewBox="0 0 32 32" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M14 20C14 19.4477 14.4477 19 15 19H17C17.5523 19 18 19.4477 18 20V24C18 24.5523 17.5523 25 17 25H15C14.4477 25 14 24.5523 14 24V20Z"
stroke="#0049B4" stroke-width="1.6" stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M11 13C11 11.3431 12.3431 10 14 10C15.6569 10 17 11.3431 17 13C17 14.6569 15.6569 16 14 16C12.3431 16 11 14.6569 11 13Z"
stroke="#0049B4" stroke -width="1.6" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M19 16L21 18L25 14" stroke="#0049B4" stroke-width="1.6"
stroke-linecap="round" stroke-linejoin="round" />
</svg> -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-journal-check" viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M10.854 6.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7.5 8.793l2.646-2.647a.5.5 0 0 1 .708 0" />
<path
d="M3 0h10a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2v-1h1v1a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v1H1V2a2 2 0 0 1 2-2" />
<path
d="M1 5v-.5a.5.5 0 0 1 1 0V5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1zm0 3v-.5a.5.5 0 0 1 1 0V8h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1zm0 3v-.5a.5.5 0 0 1 1 0v.5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z" />
</svg>
</div>
<div class="signup-step__card">
<div class="signup-step__card-num">STEP 02</div>
<div class="signup-step__card-title">승인</div>
</div>
</div>
<!-- Step 3 -->
<div class="signup-step">
<div class="signup-step__icon-box three">
<!-- <svg width="40" height="40" viewBox="0 0 32 32" fill="none"
xmlns="http://www.w3.org/2000/svg">
<rect x="7" y="7" width="18" height="18" rx="2" stroke="#0049B4" stroke-width="1.6"
stroke-linecap="round" stroke-linejoin="round" />
<path d="M12 12H20" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round" />
<path d="M12 16H20" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round" />
<path d="M12 20H16" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round" />
</svg> -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-code" viewBox="0 0 16 16">
<path
d="M5.854 4.854a.5.5 0 1 0-.708-.708l-3.5 3.5a.5.5 0 0 0 0 .708l3.5 3.5a.5.5 0 0 0 .708-.708L2.707 8zm4.292 0a.5.5 0 0 1 .708-.708l3.5 3.5a.5.5 0 0 1 0 .708l-3.5 3.5a.5.5 0 0 1-.708-.708L13.293 8z" />
</svg>
</div>
<div class="signup-step__card">
<div class="signup-step__card-num">STEP 03</div>
<div class="signup-step__card-title">API/APP 사용 신청</div>
</div>
</div>
<!-- Step 4 -->
<div class="signup-step">
<div class="signup-step__icon-box four">
<!-- <svg width="40" height="40" viewBox="0 0 32 32" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M21 8H24V16" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M8 8H11V16" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round"
stroke-linejoin="round" />
</svg> -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-display" viewBox="0 0 16 16">
<path
d="M0 4s0-2 2-2h12s2 0 2 2v6s0 2-2 2h-4q0 1 .25 1.5H11a.5.5 0 0 1 0 1H5a.5.5 0 0 1 0-1h.75Q6 13 6 12H2s-2 0-2-2zm1.398-.855a.76.76 0 0 0-.254.302A1.5 1.5 0 0 0 1 4.01V10c0 .325.078.502.145.602q.105.156.302.254a1.5 1.5 0 0 0 .538.143L2.01 11H14c.325 0 .502-.078.602-.145a.76.76 0 0 0 .254-.302 1.5 1.5 0 0 0 .143-.538L15 9.99V4c0-.325-.078-.502-.145-.602a.76.76 0 0 0-.302-.254A1.5 1.5 0 0 0 13.99 3H2c-.325 0-.502.078-.602.145" />
</svg>
</div>
<div class="signup-step__card">
<div class="signup-step__card-num">STEP 04</div>
<div class="signup-step__card-title">개발/테스트</div>
</div>
</div>
<!-- Step 5 -->
<div class="signup-step">
<div class="signup-step__icon-box five">
<!-- <svg width="40" height="40" viewBox="0 0 32 32" fill="none"-->
<!-- xmlns="http://www.w3.org/2000/svg">-->
<!-- <rect x="7" y="7" width="18" height="18" rx="2" stroke="#0049B4" stroke-width="1.6"-->
<!-- stroke-linecap="round" stroke-linejoin="round" />-->
<!-- <path d="M12 11H20" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round" />-->
<!-- <path d="M12 15L14.5 17.5L20 12" stroke="#0049B4" stroke-width="1.6"-->
<!-- stroke-linecap="round" stroke-linejoin="round" />-->
<!-- </svg>-->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-ui-checks-grid" viewBox="0 0 16 16">
<path d="M2 10h3a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1m9-9h3a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1m0 9a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1zm0-10a2 2 0 0 0-2 2v3a2 2 0 0 0 2 2h3a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zM2 9a2 2 0 0 0-2 2v3a2 2 0 0 0 2 2h3a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2zm7 2a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2zM0 2a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm5.354.854a.5.5 0 1 0-.708-.708L3 3.793l-.646-.647a.5.5 0 1 0-.708.708l1 1a.5.5 0 0 0 .708 0z"/>
</svg>
</div>
<div class="signup-step__card">
<div class="signup-step__card-num">STEP 05</div>
<div class="signup-step__card-title">운영신청 및 심사</div>
</div>
</div>
<!-- Step 6 -->
<div class="signup-step">
<div class="signup-step__icon-box six">
<!-- <svg width="40" height="40" viewBox="0 0 32 32" fill="none"
xmlns="http://www.w3.org/2000/svg">
<rect x="7" y="9" width="18" height="16" rx="2" stroke="#0049B4" stroke-width="1.6"
stroke-linecap="round" stroke-linejoin="round" />
<path d="M7 13H25" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round" />
<path d="M12 7V11" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round" />
<path d="M20 7V11" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round" />
<path d="M12 17H14" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round" />
<path d="M12 21H14" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round" />
</svg> -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-airplane" viewBox="0 0 16 16">
<path
d="M6.428 1.151C6.708.591 7.213 0 8 0s1.292.592 1.572 1.151C9.861 1.73 10 2.431 10 3v3.691l5.17 2.585a1.5 1.5 0 0 1 .83 1.342V12a.5.5 0 0 1-.582.493l-5.507-.918-.375 2.253 1.318 1.318A.5.5 0 0 1 10.5 16h-5a.5.5 0 0 1-.354-.854l1.319-1.318-.376-2.253-5.507.918A.5.5 0 0 1 0 12v-1.382a1.5 1.5 0 0 1 .83-1.342L6 6.691V3c0-.568.14-1.271.428-1.849m.894.448C7.111 2.02 7 2.569 7 3v4a.5.5 0 0 1-.276.447l-5.448 2.724a.5.5 0 0 0-.276.447v.792l5.418-.903a.5.5 0 0 1 .575.41l.5 3a.5.5 0 0 1-.14.437L6.708 15h2.586l-.647-.646a.5.5 0 0 1-.14-.436l.5-3a.5.5 0 0 1 .576-.411L15 11.41v-.792a.5.5 0 0 0-.276-.447L9.276 7.447A.5.5 0 0 1 9 7V3c0-.432-.11-.979-.322-1.401C8.458 1.159 8.213 1 8 1s-.458.158-.678.599" />
</svg>
</div>
<div class="signup-step__card">
<div class="signup-step__card-num">STEP 06</div>
<div class="signup-step__card-title">서비스 개시(예약)</div>
</div>
</div>
</div>
<div class="signup-action">
<a th:href="@{/signup}" class="signup-action__btn">회원가입하러 가기</a>
</div>
</section>
</div> </div>
</div> </div>
</th:block>
<!-- Step Process Card -->
<div class="signup-guide__card">
<div class="signup-guide__steps">
<!-- Step 1 -->
<div class="signup-guide__step">
<div class="signup-guide__step-icon">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 20C14 19.4477 14.4477 19 15 19H17C17.5523 19 18 19.4477 18 20V24C18 24.5523 17.5523 25 17 25H15C14.4477 25 14 24.5523 14 24V20Z" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 13C11 11.3431 12.3431 10 14 10C15.6569 10 17 11.3431 17 13C17 14.6569 15.6569 16 14 16C12.3431 16 11 14.6569 11 13Z" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20 21V20C20 18.8954 20.8954 18 22 18C23.1046 18 24 18.8954 24 20V21" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19 14C19 12.8954 19.8954 12 21 12C22.1046 12 23 12.8954 23 14C23 15.1046 22.1046 16 21 16C19.8954 16 19 15.1046 19 14Z" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="signup-guide__step-dot"></div>
<div class="signup-guide__step-label">
<span class="signup-guide__step-num">Step1</span>
<span class="signup-guide__step-text">회원가입</span>
</div>
<div class="signup-guide__step-line"></div>
</div>
<!-- Step 2 -->
<div class="signup-guide__step">
<div class="signup-guide__step-icon">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 20C14 19.4477 14.4477 19 15 19H17C17.5523 19 18 19.4477 18 20V24C18 24.5523 17.5523 25 17 25H15C14.4477 25 14 24.5523 14 24V20Z" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 13C11 11.3431 12.3431 10 14 10C15.6569 10 17 11.3431 17 13C17 14.6569 15.6569 16 14 16C12.3431 16 11 14.6569 11 13Z" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19 16L21 18L25 14" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="signup-guide__step-dot"></div>
<div class="signup-guide__step-label">
<span class="signup-guide__step-num">Step2</span>
<span class="signup-guide__step-text">승인</span>
</div>
<div class="signup-guide__step-line"></div>
</div>
<!-- Step 3 -->
<div class="signup-guide__step">
<div class="signup-guide__step-icon">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="7" y="7" width="18" height="18" rx="2" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 12H20" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round"/>
<path d="M12 16H20" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round"/>
<path d="M12 20H16" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round"/>
</svg>
</div>
<div class="signup-guide__step-dot"></div>
<div class="signup-guide__step-label">
<span class="signup-guide__step-num">Step3</span>
<span class="signup-guide__step-text">API/APP 사용 신청</span>
</div>
<div class="signup-guide__step-line"></div>
</div>
<!-- Step 4 -->
<div class="signup-guide__step">
<div class="signup-guide__step-icon">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 8H24V16" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 8H11V16" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="signup-guide__step-dot"></div>
<div class="signup-guide__step-label">
<span class="signup-guide__step-num">Step4</span>
<span class="signup-guide__step-text">개발/테스트</span>
</div>
<div class="signup-guide__step-line"></div>
</div>
<!-- Step 5 -->
<div class="signup-guide__step">
<div class="signup-guide__step-icon">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="7" y="7" width="18" height="18" rx="2" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 11H20" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round"/>
<path d="M12 15L14.5 17.5L20 12" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="signup-guide__step-dot"></div>
<div class="signup-guide__step-label">
<span class="signup-guide__step-num">Step5</span>
<span class="signup-guide__step-text">운영신청 및 심사</span>
</div>
<div class="signup-guide__step-line"></div>
</div>
<!-- Step 6 -->
<div class="signup-guide__step signup-guide__step--last">
<div class="signup-guide__step-icon">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="7" y="9" width="18" height="16" rx="2" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 13H25" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round"/>
<path d="M12 7V11" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round"/>
<path d="M20 7V11" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round"/>
<path d="M12 17H14" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round"/>
<path d="M12 21H14" stroke="#0049B4" stroke-width="1.6" stroke-linecap="round"/>
</svg>
</div>
<div class="signup-guide__step-dot"></div>
<div class="signup-guide__step-label">
<span class="signup-guide__step-num">Step6</span>
<span class="signup-guide__step-text">서비스 개시(예약)</span>
</div>
</div>
</div>
</div>
<!-- Notes Section -->
<div class="signup-guide__notes">
<p class="signup-guide__note">
<span class="signup-guide__note-mark"></span>
<span>회원은 법인, 개인(개발자)분으로 구분되며, 개발자 포털에서 온라인으로 회원가입 신청을 합니다.</span>
</p>
<p class="signup-guide__note">
<span class="signup-guide__note-mark"></span>
<span>포털 사용은 운영 담당자의 승인 후 가능합니다.</span>
</p>
<p class="signup-guide__note">
<span class="signup-guide__note-mark"></span>
<span>법인 관리자는 회원 승인 이후, 포털을 실제 사용할 직원(개발자)을 추가 등록해 주시기 바랍니다.</span>
</p>
</div>
<!-- Action Button -->
<div class="signup-guide__action">
<a th:href="@{/signup}" class="signup-guide__btn">회원가입 하러가기</a>
</div>
</div>
</th:block>
</body> </body>
<th:block layout:fragment="contentScript"> <th:block layout:fragment="contentScript">
@@ -9,153 +9,160 @@
<!-- Section 1: Hero --> <!-- Section 1: Hero -->
<header class="oauth2-2legged__hero"> <header class="oauth2-2legged__hero">
<div class="oauth2-2legged__hero-body"> <div class="oauth2-2legged__hero-inner">
<span class="oauth2-2legged__hero-eyebrow"> <div class="oauth2-2legged__hero-illust" aria-hidden="true">
<span class="oauth2-2legged__hero-eyebrow-dot"></span> <svg viewBox="0 0 280 200" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Server-to-Server Token Flow">
개발 가이드 · 2-Legged · Client Credentials <defs>
</span> <marker id="o2leg-hero-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto">
<h1 class="oauth2-2legged__hero-title">OAuth2 개발가이드</h1> <path d="M0,0 L10,5 L0,10 Z" fill="#0049b4"/>
<p class="oauth2-2legged__hero-lead">DJBank Open API를 호출하기 위한 ClientID/Secret 기반 토큰 발급과</p> </marker>
<p class="oauth2-2legged__hero-lead">Bearer 인증 호출 방법을 단계별로 설명합니다.</p> </defs>
<rect x="0" y="0" width="280" height="200" rx="20" fill="#FFFFFF"/>
<div class="oauth2-2legged__hero-chips"> <rect x="40" y="90" width="80" height="50" rx="8" fill="#EDF9FE" stroke="#0049b4"/>
<span class="oauth2-2legged__chip oauth2-2legged__chip--primary">Server-to-Server</span> <text x="80" y="120" text-anchor="middle" font-size="11" font-weight="700" fill="#0049b4">Client</text>
<span class="oauth2-2legged__chip">cURL 예제</span>
<span class="oauth2-2legged__chip">application/x-www-form</span> <rect x="160" y="90" width="80" height="50" rx="8" fill="#EDF9FE" stroke="#0049b4"/>
<text x="200" y="112" text-anchor="middle" font-size="10" font-weight="700" fill="#0049b4">DJBank</text>
<text x="200" y="126" text-anchor="middle" font-size="10" font-weight="700" fill="#0049b4">Open API</text>
<line x1="120" y1="115" x2="160" y2="115" stroke="#0049b4" stroke-width="2" marker-end="url(#o2leg-hero-arrow)"/>
<g transform="translate(60,30)">
<rect width="160" height="32" rx="6" fill="#1A1A2E"/>
<text x="14" y="21" font-family="'Fira Code', monospace" font-size="12" fill="#00D4FF">Bearer eyJhbG..</text>
</g>
<text x="140" y="170" text-anchor="middle" font-size="11" font-weight="600" fill="#64748B">Server-to-Server Token Flow</text>
</svg>
</div> </div>
</div>
<div class="oauth2-2legged__hero-illust" aria-hidden="true"> <div class="oauth2-2legged__hero-body">
<svg viewBox="0 0 280 200" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Server-to-Server Token Flow"> <span class="oauth2-2legged__hero-eyebrow">
<defs> <span class="oauth2-2legged__hero-eyebrow-dot"></span>
<marker id="o2leg-hero-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto"> 개발 가이드 · 2-Legged · Client Credentials
<path d="M0,0 L10,5 L0,10 Z" fill="#0049b4"/> </span>
</marker> <h1 class="oauth2-2legged__hero-title">OAuth2 개발가이드</h1>
</defs> <p class="oauth2-2legged__hero-lead">DJBank Open API를 호출하기 위한 ClientID/Secret 기반 토큰 발급과<br>Bearer 인증 호출 방법을 단계별로 설명합니다.</p>
<rect x="0" y="0" width="280" height="200" rx="20" fill="#FFFFFF"/>
<rect x="40" y="90" width="80" height="50" rx="8" fill="#EDF9FE" stroke="#0049b4"/> <!-- <div class="oauth2-2legged__hero-chips">-->
<text x="80" y="120" text-anchor="middle" font-size="11" font-weight="700" fill="#0049b4">Client</text> <!-- <span class="oauth2-2legged__chip oauth2-2legged__chip&#45;&#45;primary">Server-to-Server</span>-->
<!-- <span class="oauth2-2legged__chip">cURL 예제</span>-->
<rect x="160" y="90" width="80" height="50" rx="8" fill="#EDF9FE" stroke="#0049b4"/> <!-- <span class="oauth2-2legged__chip">application/x-www-form</span>-->
<text x="200" y="112" text-anchor="middle" font-size="10" font-weight="700" fill="#0049b4">DJBank</text> <!-- </div>-->
<text x="200" y="126" text-anchor="middle" font-size="10" font-weight="700" fill="#0049b4">Open API</text> </div>
<line x1="120" y1="115" x2="160" y2="115" stroke="#0049b4" stroke-width="2" marker-end="url(#o2leg-hero-arrow)"/>
<g transform="translate(60,30)">
<rect width="160" height="32" rx="6" fill="#1A1A2E"/>
<text x="14" y="21" font-family="'Fira Code', monospace" font-size="12" fill="#00D4FF">Bearer eyJhbG..</text>
</g>
<text x="140" y="170" text-anchor="middle" font-size="11" font-weight="600" fill="#64748B">Server-to-Server Token Flow</text>
</svg>
</div> </div>
</header> </header>
<!-- Section 2: 사전 준비 --> <div class="service-main">
<section class="oauth2-2legged__prereq" aria-labelledby="o2leg-prereq-title"> <!-- Sidebar -->
<span class="oauth2-2legged__eyebrow">PREREQUISITE</span> <th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('oauth2')}"></th:block>
<h2 class="oauth2-2legged__h2" id="o2leg-prereq-title">시작 전 준비 사항</h2>
<div class="oauth2-2legged__prereq-grid"> <div class="service-content">
<article class="oauth2-2legged__prereq-card">
<span class="oauth2-2legged__prereq-num">1</span>
<div class="oauth2-2legged__prereq-body">
<h3 class="oauth2-2legged__prereq-title">앱 등록</h3>
<p>개발자포털 [내 앱] 메뉴에서 신규</p>
<p>애플리케이션을 등록합니다.</p>
</div>
</article>
<article class="oauth2-2legged__prereq-card"> <!-- Section 2: 사전 준비 -->
<span class="oauth2-2legged__prereq-num">2</span> <section class="oauth2-2legged__prereq" aria-labelledby="o2leg-prereq-title">
<div class="oauth2-2legged__prereq-body"> <span class="oauth2-2legged__eyebrow">PREREQUISITE</span>
<h3 class="oauth2-2legged__prereq-title">ClientID / Secret 발급</h3> <h2 class="oauth2-2legged__h2" id="o2leg-prereq-title">시작 전 준비 사항</h2>
<p>승인 완료 후 발급된 자격증명을</p>
<p>서버 환경변수에 안전하게 보관.</p>
</div>
</article>
<article class="oauth2-2legged__prereq-card"> <div class="oauth2-2legged__prereq-grid">
<span class="oauth2-2legged__prereq-num">3</span> <article class="oauth2-2legged__prereq-card">
<div class="oauth2-2legged__prereq-body"> <span class="oauth2-2legged__prereq-num">1</span>
<h3 class="oauth2-2legged__prereq-title">Scope 확인</h3> <div class="oauth2-2legged__prereq-body">
<p>호출하려는 API에 필요한 scope</p> <h3 class="oauth2-2legged__prereq-title">앱 등록</h3>
<p>권한이 부여됐는지 확인합니다.</p> <p>개발자포털 [내 앱] 메뉴에서 신규</p>
</div> <p>애플리케이션을 등록합니다.</p>
</article> </div>
</div> </article>
</section>
<!-- Section 3: 전체 인증·호출 시퀀스 --> <article class="oauth2-2legged__prereq-card">
<section class="oauth2-2legged__sequence" aria-labelledby="o2leg-seq-title"> <span class="oauth2-2legged__prereq-num">2</span>
<span class="oauth2-2legged__eyebrow">SEQUENCE</span> <div class="oauth2-2legged__prereq-body">
<h2 class="oauth2-2legged__h2" id="o2leg-seq-title">전체 인증·호출 시퀀스</h2> <h3 class="oauth2-2legged__prereq-title">ClientID / Secret 발급</h3>
<p>승인 완료 후 발급된 자격증명을</p>
<p>서버 환경변수에 안전하게 보관.</p>
</div>
</article>
<div class="oauth2-2legged__sequence-diagram"> <article class="oauth2-2legged__prereq-card">
<svg viewBox="0 0 1120 282" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="OAuth 2.0 2-Legged Sequence"> <span class="oauth2-2legged__prereq-num">3</span>
<defs> <div class="oauth2-2legged__prereq-body">
<marker id="o2leg-arrow-primary" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto"> <h3 class="oauth2-2legged__prereq-title">Scope 확인</h3>
<path d="M0,0 L10,5 L0,10 Z" fill="#0049b4"/> <p>호출하려는 API에 필요한 scope</p>
</marker> <p>권한이 부여됐는지 확인합니다.</p>
<marker id="o2leg-arrow-gray" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto"> </div>
<path d="M0,0 L10,5 L0,10 Z" fill="#64748B"/> </article>
</marker> </div>
</defs> </section>
<g> <!-- Section 3: 전체 인증·호출 시퀀스 -->
<rect x="120" y="24" width="240" height="48" rx="24" fill="#EDF9FE" stroke="#0049b4"/> <section class="oauth2-2legged__sequence" aria-labelledby="o2leg-seq-title">
<text x="240" y="54" text-anchor="middle" font-size="14" font-weight="700" fill="#0049b4">Client Application</text> <span class="oauth2-2legged__eyebrow">SEQUENCE</span>
<line x1="240" y1="72" x2="240" y2="254" stroke="#94A3B8" stroke-dasharray="4 4"/> <h2 class="oauth2-2legged__h2" id="o2leg-seq-title">전체 인증·호출 시퀀스</h2>
</g>
<g>
<rect x="760" y="24" width="240" height="48" rx="24" fill="#FFFFFF" stroke="#0049b4"/>
<text x="880" y="54" text-anchor="middle" font-size="14" font-weight="700" fill="#0049b4">DJBank Open API</text>
<line x1="880" y1="72" x2="880" y2="254" stroke="#94A3B8" stroke-dasharray="4 4"/>
</g>
<text x="560" y="102" text-anchor="middle" font-size="12" font-weight="700" fill="#0049b4">① POST /dj/oauth/token</text> <div class="oauth2-2legged__sequence-diagram">
<text x="560" y="118" text-anchor="middle" font-size="11" font-weight="500" fill="#64748B">grant_type=client_credentials, client_id, client_secret, scope</text> <svg viewBox="0 0 1120 282" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="OAuth 2.0 2-Legged Sequence">
<line x1="240" y1="128" x2="880" y2="128" stroke="#0049b4" stroke-width="2" marker-end="url(#o2leg-arrow-primary)"/> <defs>
<marker id="o2leg-arrow-primary" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto">
<path d="M0,0 L10,5 L0,10 Z" fill="#0049b4"/>
</marker>
<marker id="o2leg-arrow-gray" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto">
<path d="M0,0 L10,5 L0,10 Z" fill="#64748B"/>
</marker>
</defs>
<text x="560" y="160" text-anchor="middle" font-size="12" font-weight="700" fill="#64748B">② access_token 발급</text> <g>
<line x1="880" y1="170" x2="240" y2="170" stroke="#64748B" stroke-width="2" marker-end="url(#o2leg-arrow-gray)"/> <rect x="120" y="24" width="240" height="48" rx="24" fill="#EDF9FE" stroke="#0049b4"/>
<text x="560" y="188" text-anchor="middle" font-size="11" font-weight="500" fill="#64748B">{ access_token, token_type:"bearer", expires_in:86400, scope, jti }</text> <text x="240" y="54" text-anchor="middle" font-size="14" font-weight="700" fill="#0049b4">Client Application</text>
<line x1="240" y1="72" x2="240" y2="254" stroke="#94A3B8" stroke-dasharray="4 4"/>
</g>
<g>
<rect x="760" y="24" width="240" height="48" rx="24" fill="#FFFFFF" stroke="#0049b4"/>
<text x="880" y="54" text-anchor="middle" font-size="14" font-weight="700" fill="#0049b4">DJBank Open API</text>
<line x1="880" y1="72" x2="880" y2="254" stroke="#94A3B8" stroke-dasharray="4 4"/>
</g>
<text x="560" y="216" text-anchor="middle" font-size="12" font-weight="700" fill="#0049b4">③ GET /api/v1/... · Authorization: Bearer &lt;access_token&gt;</text> <text x="560" y="102" text-anchor="middle" font-size="12" font-weight="700" fill="#0049b4">① POST /dj/oauth/token</text>
<line x1="240" y1="226" x2="880" y2="226" stroke="#0049b4" stroke-width="2" marker-end="url(#o2leg-arrow-primary)"/> <text x="560" y="118" text-anchor="middle" font-size="11" font-weight="500" fill="#64748B">grant_type=client_credentials, client_id, client_secret, scope</text>
<line x1="240" y1="128" x2="880" y2="128" stroke="#0049b4" stroke-width="2" marker-end="url(#o2leg-arrow-primary)"/>
<text x="560" y="248" text-anchor="middle" font-size="12" font-weight="700" fill="#64748B">④ 검증 후 API 응답</text> <text x="560" y="160" text-anchor="middle" font-size="12" font-weight="700" fill="#64748B">② access_token 발급</text>
<line x1="880" y1="254" x2="240" y2="254" stroke="#64748B" stroke-width="2" marker-end="url(#o2leg-arrow-gray)"/> <line x1="880" y1="170" x2="240" y2="170" stroke="#64748B" stroke-width="2" marker-end="url(#o2leg-arrow-gray)"/>
</svg> <text x="560" y="188" text-anchor="middle" font-size="11" font-weight="500" fill="#64748B">{ access_token, token_type:"bearer", expires_in:86400, scope, jti }</text>
</div>
</section>
<!-- Section 4: STEP 1 — 액세스 토큰 요청 --> <text x="560" y="216" text-anchor="middle" font-size="12" font-weight="700" fill="#0049b4">③ GET /api/v1/... · Authorization: Bearer &lt;access_token&gt;</text>
<section class="oauth2-2legged__step" aria-labelledby="o2leg-step1-title"> <line x1="240" y1="226" x2="880" y2="226" stroke="#0049b4" stroke-width="2" marker-end="url(#o2leg-arrow-primary)"/>
<span class="oauth2-2legged__eyebrow">STEP 1</span>
<h2 class="oauth2-2legged__h2" id="o2leg-step1-title">액세스 토큰 요청</h2>
<p class="oauth2-2legged__desc">ClientID/Secret 으로 토큰 엔드포인트에 POST 합니다.</p>
<div class="oauth2-2legged__endpoint-box"> <text x="560" y="248" text-anchor="middle" font-size="12" font-weight="700" fill="#64748B">④ 검증 후 API 응답</text>
<span class="oauth2-2legged__method">POST</span> <line x1="880" y1="254" x2="240" y2="254" stroke="#64748B" stroke-width="2" marker-end="url(#o2leg-arrow-gray)"/>
<code class="oauth2-2legged__endpoint-path">https://openapi.djbank.co.kr/dj/oauth/token</code> </svg>
<span class="oauth2-2legged__endpoint-content-type">x-www-form-urlencoded</span> </div>
</div> </section>
<div class="oauth2-2legged__step-grid"> <!-- Section 4: STEP 1 — 액세스 토큰 요청 -->
<div class="oauth2-2legged__panel"> <section class="oauth2-2legged__step" aria-labelledby="o2leg-step1-title">
<h3 class="oauth2-2legged__panel-title">요청 파라미터</h3> <span class="oauth2-2legged__eyebrow">STEP 1</span>
<table class="oauth2-2legged__table"> <h2 class="oauth2-2legged__h2" id="o2leg-step1-title">액세스 토큰 요청</h2>
<thead> <p class="oauth2-2legged__desc">ClientID/Secret 으로 토큰 엔드포인트에 POST 합니다.</p>
<div class="oauth2-2legged__endpoint-box">
<span class="oauth2-2legged__method">POST</span>
<code class="oauth2-2legged__endpoint-path">https://openapi.djbank.co.kr/dj/oauth/token</code>
<span class="oauth2-2legged__endpoint-content-type">x-www-form-urlencoded</span>
</div>
<div class="oauth2-2legged__step-grid">
<div class="oauth2-2legged__panel">
<h3 class="oauth2-2legged__panel-title">요청 파라미터</h3>
<table class="oauth2-2legged__table">
<thead>
<tr> <tr>
<th>PARAMETER</th> <th>PARAMETER</th>
<th>REQ</th> <th>REQ</th>
<th>설명</th> <th>설명</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td><code>grant_type</code></td> <td><code>grant_type</code></td>
<td><span class="oauth2-2legged__req-badge oauth2-2legged__req-badge--required">필수</span></td> <td><span class="oauth2-2legged__req-badge oauth2-2legged__req-badge--required">필수</span></td>
@@ -176,14 +183,14 @@
<td><span class="oauth2-2legged__req-badge oauth2-2legged__req-badge--optional">선택</span></td> <td><span class="oauth2-2legged__req-badge oauth2-2legged__req-badge--optional">선택</span></td>
<td>호출 권한 범위 (공백 구분)</td> <td>호출 권한 범위 (공백 구분)</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<p class="oauth2-2legged__warning">⚠ Content-Type: application/x-www-form-urlencoded 헤더 필수</p> <p class="oauth2-2legged__warning">⚠ Content-Type: application/x-www-form-urlencoded 헤더 필수</p>
</div> </div>
<div class="oauth2-2legged__code-panel"> <div class="oauth2-2legged__code-panel">
<span class="oauth2-2legged__code-tag">cURL</span> <span class="oauth2-2legged__code-tag">cURL</span>
<pre class="oauth2-2legged__code-block"><span class="o2leg-cm"># 액세스 토큰 발급</span> <pre class="oauth2-2legged__code-block"><span class="o2leg-cm"># 액세스 토큰 발급</span>
curl -X <span class="o2leg-y">POST</span> <span class="o2leg-c">'https://openapi.djbank.co.kr/dj/oauth/token'</span> \ curl -X <span class="o2leg-y">POST</span> <span class="o2leg-c">'https://openapi.djbank.co.kr/dj/oauth/token'</span> \
-H <span class="o2leg-c">'Content-Type: application/x-www-form-urlencoded'</span> \ -H <span class="o2leg-c">'Content-Type: application/x-www-form-urlencoded'</span> \
-d <span class="o2leg-c">'grant_type=client_credentials'</span> \ -d <span class="o2leg-c">'grant_type=client_credentials'</span> \
@@ -192,20 +199,20 @@ curl -X <span class="o2leg-y">POST</span> <span class="o2leg-c">'https://openapi
-d <span class="o2leg-c">'scope=read.accounts'</span> -d <span class="o2leg-c">'scope=read.accounts'</span>
<span class="o2leg-g"># 응답: 200 OK + JSON</span></pre> <span class="o2leg-g"># 응답: 200 OK + JSON</span></pre>
</div>
</div> </div>
</div> </section>
</section>
<!-- Section 5: STEP 2 — 액세스 토큰 응답 --> <!-- Section 5: STEP 2 — 액세스 토큰 응답 -->
<section class="oauth2-2legged__step" aria-labelledby="o2leg-step2-title"> <section class="oauth2-2legged__step" aria-labelledby="o2leg-step2-title">
<span class="oauth2-2legged__eyebrow">STEP 2</span> <span class="oauth2-2legged__eyebrow">STEP 2</span>
<h2 class="oauth2-2legged__h2" id="o2leg-step2-title">액세스 토큰 응답</h2> <h2 class="oauth2-2legged__h2" id="o2leg-step2-title">액세스 토큰 응답</h2>
<p class="oauth2-2legged__desc">정상 발급 시 200 OK 와 함께 다음 JSON 을 반환합니다.</p> <p class="oauth2-2legged__desc">정상 발급 시 200 OK 와 함께 다음 JSON 을 반환합니다.</p>
<div class="oauth2-2legged__step-grid"> <div class="oauth2-2legged__step-grid">
<div class="oauth2-2legged__code-panel"> <div class="oauth2-2legged__code-panel">
<span class="oauth2-2legged__code-tag oauth2-2legged__code-tag--ok">200 OK · JSON</span> <span class="oauth2-2legged__code-tag oauth2-2legged__code-tag--ok">200 OK · JSON</span>
<pre class="oauth2-2legged__code-block">{ <pre class="oauth2-2legged__code-block">{
<span class="o2leg-c">"access_token"</span>: <span class="o2leg-y">"eyJhbGciOiJSUzI1NiJ9..."</span>, <span class="o2leg-c">"access_token"</span>: <span class="o2leg-y">"eyJhbGciOiJSUzI1NiJ9..."</span>,
<span class="o2leg-c">"token_type"</span>: <span class="o2leg-y">"bearer"</span>, <span class="o2leg-c">"token_type"</span>: <span class="o2leg-y">"bearer"</span>,
<span class="o2leg-c">"expires_in"</span>: <span class="o2leg-p">86400</span>, <span class="o2leg-c">"expires_in"</span>: <span class="o2leg-p">86400</span>,
@@ -214,19 +221,19 @@ curl -X <span class="o2leg-y">POST</span> <span class="o2leg-c">'https://openapi
} }
<span class="o2leg-g"># 응답 본문은 캐싱하여 expires_in 동안 재사용 가능</span></pre> <span class="o2leg-g"># 응답 본문은 캐싱하여 expires_in 동안 재사용 가능</span></pre>
</div> </div>
<div class="oauth2-2legged__panel"> <div class="oauth2-2legged__panel">
<h3 class="oauth2-2legged__panel-title">응답 필드</h3> <h3 class="oauth2-2legged__panel-title">응답 필드</h3>
<table class="oauth2-2legged__table"> <table class="oauth2-2legged__table">
<thead> <thead>
<tr> <tr>
<th>FIELD</th> <th>FIELD</th>
<th>TYPE</th> <th>TYPE</th>
<th>설명</th> <th>설명</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td><code class="oauth2-2legged__field-name">access_token</code></td> <td><code class="oauth2-2legged__field-name">access_token</code></td>
<td>string</td> <td>string</td>
@@ -252,38 +259,38 @@ curl -X <span class="o2leg-y">POST</span> <span class="o2leg-c">'https://openapi
<td>string</td> <td>string</td>
<td>토큰 고유 ID (감사/취소용)</td> <td>토큰 고유 ID (감사/취소용)</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div>
</div> </div>
</div> </section>
</section>
<!-- Section 6: STEP 3 — API 호출 --> <!-- Section 6: STEP 3 — API 호출 -->
<section class="oauth2-2legged__step" aria-labelledby="o2leg-step3-title"> <section class="oauth2-2legged__step" aria-labelledby="o2leg-step3-title">
<span class="oauth2-2legged__eyebrow">STEP 3</span> <span class="oauth2-2legged__eyebrow">STEP 3</span>
<h2 class="oauth2-2legged__h2" id="o2leg-step3-title">발급 토큰으로 API 호출</h2> <h2 class="oauth2-2legged__h2" id="o2leg-step3-title">발급 토큰으로 API 호출</h2>
<p class="oauth2-2legged__desc">Authorization 헤더에 Bearer 토큰을 실어 보호 자원 API 를 호출합니다.</p> <p class="oauth2-2legged__desc">Authorization 헤더에 Bearer 토큰을 실어 보호 자원 API 를 호출합니다.</p>
<div class="oauth2-2legged__step-grid"> <div class="oauth2-2legged__step-grid">
<div class="oauth2-2legged__panel"> <div class="oauth2-2legged__panel">
<h3 class="oauth2-2legged__panel-title">필수 헤더</h3> <h3 class="oauth2-2legged__panel-title">필수 헤더</h3>
<div class="oauth2-2legged__header-box"> <div class="oauth2-2legged__header-box">
<code class="oauth2-2legged__header-key">Authorization:</code> <code class="oauth2-2legged__header-key">Authorization:</code>
<code class="oauth2-2legged__header-value">Bearer eyJhbGciOiJSUzI1NiJ9...</code> <code class="oauth2-2legged__header-value">Bearer eyJhbGciOiJSUzI1NiJ9...</code>
</div>
<h4 class="oauth2-2legged__panel-subtitle">토큰 재사용 가이드</h4>
<ul class="oauth2-2legged__tips">
<li>동일 토큰은 expires_in(기본 86400초) 동안 재사용</li>
<li>만료 임박 시 재발급 후 교체 (예: TTL의 80% 시점)</li>
<li>매 호출마다 토큰을 새로 발급하지 마세요</li>
<li>권한이 다른 API 는 scope 별로 토큰을 분리 발급</li>
</ul>
</div> </div>
<h4 class="oauth2-2legged__panel-subtitle">토큰 재사용 가이드</h4> <div class="oauth2-2legged__code-panel">
<ul class="oauth2-2legged__tips"> <span class="oauth2-2legged__code-tag">cURL</span>
<li>동일 토큰은 expires_in(기본 86400초) 동안 재사용</li> <pre class="oauth2-2legged__code-block"><span class="o2leg-cm"># 보호 자원 API 호출</span>
<li>만료 임박 시 재발급 후 교체 (예: TTL의 80% 시점)</li>
<li>매 호출마다 토큰을 새로 발급하지 마세요</li>
<li>권한이 다른 API 는 scope 별로 토큰을 분리 발급</li>
</ul>
</div>
<div class="oauth2-2legged__code-panel">
<span class="oauth2-2legged__code-tag">cURL</span>
<pre class="oauth2-2legged__code-block"><span class="o2leg-cm"># 보호 자원 API 호출</span>
curl -X <span class="o2leg-y">GET</span> \ curl -X <span class="o2leg-y">GET</span> \
<span class="o2leg-c">'https://openapi.djbank.co.kr/api/v1/accounts'</span> \ <span class="o2leg-c">'https://openapi.djbank.co.kr/api/v1/accounts'</span> \
-H <span class="o2leg-c">'Authorization: Bearer eyJhbGciOiJSUzI..'</span> \ -H <span class="o2leg-c">'Authorization: Bearer eyJhbGciOiJSUzI..'</span> \
@@ -292,26 +299,26 @@ curl -X <span class="o2leg-y">GET</span> \
<span class="o2leg-g"># 응답</span> <span class="o2leg-g"># 응답</span>
HTTP/1.1 <span class="o2leg-g">200 OK</span> HTTP/1.1 <span class="o2leg-g">200 OK</span>
{ <span class="o2leg-c">"accounts"</span>: [ ... ] }</pre> { <span class="o2leg-c">"accounts"</span>: [ ... ] }</pre>
</div>
</div> </div>
</div> </section>
</section>
<!-- Section 7: 대표 에러 응답 (표) --> <!-- Section 7: 대표 에러 응답 (표) -->
<section class="oauth2-2legged__errors" aria-labelledby="o2leg-errors-title"> <section class="oauth2-2legged__errors" aria-labelledby="o2leg-errors-title">
<span class="oauth2-2legged__eyebrow">ERROR CODES</span> <span class="oauth2-2legged__eyebrow">ERROR CODES</span>
<h2 class="oauth2-2legged__h2" id="o2leg-errors-title">대표 에러 응답</h2> <h2 class="oauth2-2legged__h2" id="o2leg-errors-title">대표 에러 응답</h2>
<div class="oauth2-2legged__panel"> <div class="oauth2-2legged__panel">
<table class="oauth2-2legged__table oauth2-2legged__table--errors"> <table class="oauth2-2legged__table oauth2-2legged__table--errors">
<thead> <thead>
<tr> <tr>
<th>HTTP</th> <th>HTTP</th>
<th>ERROR CODE</th> <th>ERROR CODE</th>
<th>의미</th> <th>의미</th>
<th>해결 가이드</th> <th>해결 가이드</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td><span class="oauth2-2legged__status-badge oauth2-2legged__status-badge--400">400</span></td> <td><span class="oauth2-2legged__status-badge oauth2-2legged__status-badge--400">400</span></td>
<td><code>invalid_request</code></td> <td><code>invalid_request</code></td>
@@ -336,21 +343,21 @@ HTTP/1.1 <span class="o2leg-g">200 OK</span>
<td>scope 권한 부족</td> <td>scope 권한 부족</td>
<td>필요 scope 추가 신청 후 재발급</td> <td>필요 scope 추가 신청 후 재발급</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</section> </section>
<!-- Section 8: 에러 응답 예시 (단일 사례) --> <!-- Section 8: 에러 응답 예시 (단일 사례) -->
<section class="oauth2-2legged__error-example" aria-labelledby="o2leg-err-ex-title"> <section class="oauth2-2legged__error-example" aria-labelledby="o2leg-err-ex-title">
<span class="oauth2-2legged__eyebrow">ERROR RESPONSE</span> <span class="oauth2-2legged__eyebrow">ERROR RESPONSE</span>
<h2 class="oauth2-2legged__h2" id="o2leg-err-ex-title">에러 응답 예시</h2> <h2 class="oauth2-2legged__h2" id="o2leg-err-ex-title">에러 응답 예시</h2>
<p class="oauth2-2legged__desc">자격증명 불일치 시 401 Unauthorized 와 함께 다음 JSON 을 반환합니다.</p> <p class="oauth2-2legged__desc">자격증명 불일치 시 401 Unauthorized 와 함께 다음 JSON 을 반환합니다.</p>
<div class="oauth2-2legged__step-grid"> <div class="oauth2-2legged__step-grid">
<div class="oauth2-2legged__code-panel oauth2-2legged__code-panel--error"> <div class="oauth2-2legged__code-panel oauth2-2legged__code-panel--error">
<span class="oauth2-2legged__code-tag oauth2-2legged__code-tag--error">401 Unauthorized · JSON</span> <span class="oauth2-2legged__code-tag oauth2-2legged__code-tag--error">401 Unauthorized · JSON</span>
<pre class="oauth2-2legged__code-block">{ <pre class="oauth2-2legged__code-block">{
<span class="o2leg-c">"error"</span>: <span class="o2leg-y">"invalid_client"</span>, <span class="o2leg-c">"error"</span>: <span class="o2leg-y">"invalid_client"</span>,
<span class="o2leg-c">"error_description"</span>: <span class="o2leg-c">"error_description"</span>:
<span class="o2leg-y">"Client authentication failed"</span>, <span class="o2leg-y">"Client authentication failed"</span>,
@@ -359,19 +366,19 @@ HTTP/1.1 <span class="o2leg-g">200 OK</span>
} }
<span class="o2leg-g"># 처리: ClientID·Secret 확인 후 재시도</span></pre> <span class="o2leg-g"># 처리: ClientID·Secret 확인 후 재시도</span></pre>
</div> </div>
<div class="oauth2-2legged__panel"> <div class="oauth2-2legged__panel">
<h3 class="oauth2-2legged__panel-title">응답 필드</h3> <h3 class="oauth2-2legged__panel-title">응답 필드</h3>
<table class="oauth2-2legged__table"> <table class="oauth2-2legged__table">
<thead> <thead>
<tr> <tr>
<th>FIELD</th> <th>FIELD</th>
<th>TYPE</th> <th>TYPE</th>
<th>설명</th> <th>설명</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td><code class="oauth2-2legged__field-name">error</code></td> <td><code class="oauth2-2legged__field-name">error</code></td>
<td>string</td> <td>string</td>
@@ -392,25 +399,27 @@ HTTP/1.1 <span class="o2leg-g">200 OK</span>
<td>string</td> <td>string</td>
<td>에러 발생 시각 (ISO 8601)</td> <td>에러 발생 시각 (ISO 8601)</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<p class="oauth2-2legged__error-note">⚠ error 코드는 RFC 6749 표준 코드 또는 DJBank 확장 코드</p> <p class="oauth2-2legged__error-note">⚠ error 코드는 RFC 6749 표준 코드 또는 DJBank 확장 코드</p>
</div>
</div> </div>
</div> </section>
</section>
<!-- Section 9: API 목록 CTA --> <!-- Section 9: API 목록 CTA -->
<a class="oauth2-2legged__cta" th:href="@{/apis}"> <a class="oauth2-2legged__cta" th:href="@{/apis}">
<div class="oauth2-2legged__cta-body"> <div class="oauth2-2legged__cta-body">
<span class="oauth2-2legged__cta-eyebrow">EXPLORE</span> <span class="oauth2-2legged__cta-eyebrow">EXPLORE</span>
<h2 class="oauth2-2legged__cta-title">API 목록 보러가기</h2> <h2 class="oauth2-2legged__cta-title">API 목록 보러가기</h2>
<p class="oauth2-2legged__cta-desc">사용 가능한 DJBank Open API 카탈로그를 확인하세요.</p> <p class="oauth2-2legged__cta-desc">사용 가능한 DJBank Open API 카탈로그를 확인하세요.</p>
<span class="oauth2-2legged__cta-button">API 목록 →</span> <span class="oauth2-2legged__cta-button">API 목록 →</span>
</div> </div>
<span class="oauth2-2legged__cta-deco oauth2-2legged__cta-deco--lg" aria-hidden="true"></span> <span class="oauth2-2legged__cta-deco oauth2-2legged__cta-deco--lg" aria-hidden="true"></span>
<span class="oauth2-2legged__cta-deco oauth2-2legged__cta-deco--sm" aria-hidden="true"></span> <span class="oauth2-2legged__cta-deco oauth2-2legged__cta-deco--sm" aria-hidden="true"></span>
</a> </a>
</div>
</div>
</section> </section>
</th:block> </th:block>
@@ -3,19 +3,22 @@
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/djbank_title_layout}"> xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/djbank_title_layout}">
<body> <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"> <th:block layout:fragment="contentFragment">
<section class="user-management-container"> <div class="service-main" style="padding-top: 40px;">
<!-- Sidebar -->
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('users')}"></th:block>
<!-- Main Content -->
<section class="service-content">
<section class="user-management-container" style="padding-top: 0;">
<!-- App List Title Section --> <!-- App List Title Section -->
<div class="table-controls"> <div class="table-controls">
<h2 class="total-count"> <h2 class="total-count">
<strong th:text="${(users != null ? users.size() : 0) + (pendingUsers != null ? pendingUsers.size() : 0)}">0</strong> <strong th:text="${(users != null ? users.size() : 0) + (pendingUsers != null ? pendingUsers.size() : 0)}">0</strong>
</h2> </h2>
<div class="search-box"> <div class="search-box">
<button type="button" class="btn btn-primary" id="addUserBtn"> <button type="button" class="btn btn-primary" id="addUserBtn">
@@ -26,37 +29,37 @@
</div> </div>
<!-- User Table --> <!-- User Table -->
<div class="list-table" th:if="${(users != null and !users.isEmpty()) or (pendingUsers != null and !pendingUsers.isEmpty())}"> <div class="board-table board-table--user-list" th:if="${(users != null and !users.isEmpty()) or (pendingUsers != null and !pendingUsers.isEmpty())}">
<!-- Table Header --> <!-- Table Header -->
<div class="list-table-header"> <div class="board-table-header">
<div class="header-cell" style="width: 90px;">NO.</div> <div class="header-cell">NO.</div>
<div class="header-cell" style="width: 90px;">성명</div> <div class="header-cell">성명</div>
<div class="header-cell" style="flex: 1; min-width: 180px;">이메일 / 휴대폰</div> <div class="header-cell">이메일 / 휴대폰</div>
<div class="header-cell" style="width: 120px;">권한</div> <div class="header-cell">권한</div>
<div class="header-cell" style="width: 120px;">계정상태</div> <div class="header-cell">계정상태</div>
<div class="header-cell" style="width: 320px;">설정</div> <div class="header-cell">설정</div>
</div> </div>
<!-- Table Body --> <!-- Table Body -->
<div class="list-table-body"> <div class="board-table-body">
<!-- Active/Inactive Users --> <!-- Active/Inactive Users -->
<div class="list-table-row" th:each="user, status : ${users}"> <div class="board-table-row" th:each="user, status : ${users}">
<div class="row-cell" style="width: 90px;" data-label="NO" th:text="${status.index + 1}">1</div> <div class="row-cell" data-label="NO" th:text="${status.index + 1}">1</div>
<div class="row-cell" style="width: 90px;" data-label="성명"> <div class="row-cell" data-label="성명">
<a th:href="@{/users/detail(id=${user.id})}" th:text="${user.maskedUserName}"> <a th:href="@{/users/detail(id=${user.id})}" th:text="${user.maskedUserName}">
사용자 이름 사용자 이름
</a> </a>
</div> </div>
<div class="row-cell" style="flex: 1; min-width: 180px;" data-label="이메일" th:text="${user.maskedEmailAddr}"> <div class="row-cell" data-label="이메일" th:text="${user.maskedEmailAddr}">
user@example.com user@example.com
</div> </div>
<div class="row-cell" style="width: 120px;" data-label="권한"> <div class="row-cell" data-label="권한">
<span th:if="${user.roleCode != null}" th:text="#{${user.roleCode}}">권한</span> <span th:if="${user.roleCode != null}" th:text="#{${user.roleCode}}">권한</span>
</div> </div>
<div class="row-cell" style="width: 120px;" data-label="계정상태" th:text="${user.userStatus.getDescription()}"> <div class="row-cell" data-label="계정상태" th:text="${user.userStatus.getDescription()}">
활성 활성
</div> </div>
<div class="row-cell row-actions" style="width: 320px;"> <div class="row-cell row-actions">
<!-- Current User --> <!-- Current User -->
<span class="user-current-badge" th:if="${user.id == currentUserId}"> <span class="user-current-badge" th:if="${user.id == currentUserId}">
<i class="fas fa-user-check"></i> 현재 사용자 <i class="fas fa-user-check"></i> 현재 사용자
@@ -146,15 +149,15 @@
</div> </div>
<!-- Pending Invitation Users --> <!-- Pending Invitation Users -->
<div class="list-table-row" th:each="user : ${pendingUsers}"> <div class="board-table-row" th:each="user : ${pendingUsers}">
<div class="row-cell" style="width: 90px;" data-label="NO">-</div> <div class="row-cell" data-label="NO">-</div>
<div class="row-cell" style="width: 90px;" data-label="성명">-</div> <div class="row-cell" data-label="성명">-</div>
<div class="row-cell" style="flex: 1; min-width: 180px;" data-label="휴대폰" th:text="${user.maskedMobileNumber}"> <div class="row-cell" data-label="휴대폰" th:text="${user.maskedMobileNumber}">
010-****-**** 010-****-****
</div> </div>
<div class="row-cell" style="width: 120px;" data-label="권한">이용자</div> <div class="row-cell" data-label="권한">이용자</div>
<div class="row-cell" style="width: 120px;" data-label="계정상태">초대중</div> <div class="row-cell" data-label="계정상태">초대중</div>
<div class="row-cell row-actions" style="width: 320px;"> <div class="row-cell row-actions">
<!-- Desktop --> <!-- Desktop -->
<div class="actions-desktop"> <div class="actions-desktop">
<button type="button" class="list-table-btn list-table-btn--secondary cancel_invitation" <button type="button" class="list-table-btn list-table-btn--secondary cancel_invitation"
@@ -198,7 +201,9 @@
<div class="pagination page_top" <div class="pagination page_top"
th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}"></div> th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}"></div>
</section> </section>
</section>
</div>
</th:block> </th:block>
<section layout:fragment="pagePopups" class="content pop"> <section layout:fragment="pagePopups" class="content pop">
@@ -21,11 +21,18 @@
<!-- Desktop Header Layout --> <!-- Desktop Header Layout -->
<div class="desktop-header"> <div class="desktop-header">
<div class="header-left"> <div class="header-left">
<div class="logo-wrapper"> <!-- <div class="logo-wrapper">-->
<a th:href="@{/}" class="logo-link"> <!-- <a th:href="@{/}" class="logo-link">-->
<img src="/img/logo/logo-djb.png" alt="DJBank" class="logo" width="114" height="32"> <!-- <img src="/img/logo/logo-djb.png" alt="DJBank" class="logo" width="114" height="32">-->
</a> <!-- </a>-->
<a th:href="@{/}" class="logo-text">API Portal</a> <!-- <a th:href="@{/}" class="logo-text">API Portal</a>-->
<!-- </div>-->
<div >
<div class="logo">
<a th:href="@{/}" class="logo-wrapper">
<span class="logo-text-bold">DJBank</span> <span class="logo-text-thin">API Portal</span>
</a>
</div>
</div> </div>
</div> </div>
@@ -55,10 +62,15 @@
<div class="header-right"> <div class="header-right">
<!-- Login State (Anonymous) --> <!-- Login State (Anonymous) -->
<div class="auth-group" sec:authorize="isAnonymous()"> <!-- <div class="auth-group" sec:authorize="isAnonymous()">-->
<a th:href="@{/login}" class="login-btn login-btn-box"> <!-- <a th:href="@{/login}" class="login-btn login-btn-box">-->
<img th:src="@{/img/user_icon.svg}" alt="User" class="user-icon">로그인</a> <!-- <img th:src="@{/img/user_icon.svg}" alt="User" class="user-icon">로그인</a>-->
<!-- </div>-->
<div class="auth-group" sec:authorize="isAnonymous()">
<a th:href="@{/login}" class="login-btn login-btn-box">로그인</a>
<a href="#" class="btn-signup">회원가입</a>
</div> </div>
<!-- Logout State (Authenticated) --> <!-- Logout State (Authenticated) -->
@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<!-- Service Left Sidebar Fragment -->
<aside class="service-sidebar" th:fragment="sidebar(activeMenu)">
<nav class="service-nav">
<!-- 서비스 소개 그룹 (Service) -->
<th:block th:if="${activeMenu == 'intro' or activeMenu == 'guide' or activeMenu == 'oauth2'}">
<a th:href="@{/service/intro}"
th:classappend="${activeMenu == 'intro'} ? 'service-nav__item--active' : ''"
class="service-nav__item">API 포탈 소개</a>
<a th:href="@{/service/guide}"
th:classappend="${activeMenu == 'guide'} ? 'service-nav__item--active' : ''"
class="service-nav__item">회원가입 안내</a>
<a th:href="@{/service/oauth2-guide}"
th:classappend="${activeMenu == 'oauth2'} ? 'service-nav__item--active' : ''"
class="service-nav__item">OAuth2 개발가이드</a>
</th:block>
<!-- 고객지원 그룹 (Customer Support) -->
<th:block th:if="${activeMenu == 'notice' or activeMenu == 'faq' or activeMenu == 'qna' or activeMenu == 'feedback'}">
<a th:href="@{/portalnotice}"
th:classappend="${activeMenu == 'notice'} ? 'service-nav__item--active' : ''"
class="service-nav__item">공지사항</a>
<a th:href="@{/faq_list}"
th:classappend="${activeMenu == 'faq'} ? 'service-nav__item--active' : ''"
class="service-nav__item">FAQ</a>
<a th:href="@{/inquiry}"
th:classappend="${activeMenu == 'qna'} ? 'service-nav__item--active' : ''"
class="service-nav__item">Q&A</a>
<a th:href="@{/partnership}"
th:classappend="${activeMenu == 'feedback'} ? 'service-nav__item--active' : ''"
class="service-nav__item">피드백/개선요청</a>
</th:block>
<!-- 마이페이지 그룹 (My Page) -->
<th:block th:if="${activeMenu == 'users' or activeMenu == 'apiKey' or activeMenu == 'statistics' or activeMenu == 'profile' or activeMenu == 'password'}">
<!-- Profile Section -->
<div class="service-sidebar__profile">
<div class="avatar">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
</svg>
</div>
<p class="user-name"><span th:text="${#authentication.principal.userName}">사용자</span></p>
</div>
<div class="service-sidebar__divider"></div>
<a th:href="@{/users}"
th:classappend="${activeMenu == 'users'} ? 'service-nav__item--active' : ''"
class="service-nav__item"
sec:authorize="hasRole('ROLE_CORP_MANAGER')">이용자 관리</a>
<a th:href="@{/myapikey}"
th:classappend="${activeMenu == 'apiKey'} ? 'service-nav__item--active' : ''"
class="service-nav__item">인증 키 관리</a>
<a th:href="@{/statistics/daily}"
th:classappend="${activeMenu == 'statistics'} ? 'service-nav__item--active' : ''"
class="service-nav__item">이용통계</a>
<a th:href="@{/mypage/update_user}"
th:classappend="${activeMenu == 'profile'} ? 'service-nav__item--active' : ''"
class="service-nav__item">내정보 관리</a>
<a th:href="@{/mypage/password}"
th:classappend="${activeMenu == 'password'} ? 'service-nav__item--active' : ''"
class="service-nav__item">비밀번호 변경</a>
</th:block>
</nav>
</aside>
</body>
</html>