2-Legged 가이드 삭제 완료

- 관련 HTML 제거
- 메뉴 및 경로 수정
This commit is contained in:
Rinjae
2026-06-12 17:11:13 +09:00
parent 2d67647c8f
commit 23235bb854
12 changed files with 430 additions and 1814 deletions
@@ -1,24 +1,41 @@
package com.eactive.apim.portal.common.breadcrumb;
import java.util.HashMap;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.yaml.snakeyaml.Yaml;
@Service
public class PageService {
private static final Logger log = LoggerFactory.getLogger(PageService.class);
private final PageStructure pageStructure;
@Value("${portal.dev.hot-reload-pages:false}")
private boolean hotReloadEnabled;
private volatile long lastReloadedMtime = 0L;
private volatile Path watchedPath;
public PageService(PageStructure pageStructure) {
this.pageStructure = pageStructure;
}
public List<Map> getBreadcrumb(String currentPath) {
// URL에서 기본 경로와 쿼리 파라미터 분리
maybeReloadPages();
String basePath = currentPath.contains("?") ?
currentPath.substring(0, currentPath.indexOf("?")) : currentPath;
Map<String, String> queryParams = parseQueryParams(currentPath);
@@ -29,12 +46,59 @@ public class PageService {
}
public String getPageName(String currentPath) {
maybeReloadPages();
String basePath = currentPath.contains("?") ?
currentPath.substring(0, currentPath.indexOf("?")) : currentPath;
Map<String, String> queryParams = parseQueryParams(currentPath);
return findPageName(pageStructure.getHome(), basePath, queryParams);
}
// -----------------------------------------------------------------------------
// Dev-only hot reload of the `page:` tree.
// Toggle via `portal.dev.hot-reload-pages: true` in any active profile yml.
// Only the page tree (breadcrumb / menu names) reloads — `portal.pages` route
// mappings are still registered at boot and require a restart.
// -----------------------------------------------------------------------------
private void maybeReloadPages() {
if (!hotReloadEnabled) return;
try {
Path p = resolveYmlPath();
if (p == null) return;
long mtime = Files.getLastModifiedTime(p).toMillis();
if (mtime == lastReloadedMtime) return;
try (InputStream is = Files.newInputStream(p)) {
Yaml yaml = new Yaml();
Map<String, Object> root = yaml.load(is);
Object page = (root != null) ? root.get("page") : null;
if (page instanceof Map) {
Object home = ((Map<?, ?>) page).get("home");
if (home instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> homeMap = (Map<String, Object>) home;
pageStructure.setHome(homeMap);
lastReloadedMtime = mtime;
log.debug("[PageService] page tree reloaded from {} (mtime={})", p, mtime);
}
}
}
} catch (Exception e) {
log.warn("[PageService] page tree reload failed: {}", e.toString());
}
}
private Path resolveYmlPath() {
if (watchedPath != null) return watchedPath;
try {
ClassPathResource res = new ClassPathResource("application.yml");
if (!res.exists() || !res.isFile()) return null;
watchedPath = res.getFile().toPath();
return watchedPath;
} catch (IOException e) {
return null;
}
}
private Map<String, String> parseQueryParams(String path) {
Map<String, String> params = new HashMap<>();
if (path.contains("?")) {
@@ -52,7 +116,6 @@ public class PageService {
private String findPageName(Map<String, Object> currentNode, String basePath, Map<String, String> queryParams) {
String path = (String) currentNode.get("path");
// 기본 경로가 일치하는지 확인
if (path != null) {
String nodePath = path.contains("?") ?
path.substring(0, path.indexOf("?")) : path;
@@ -63,7 +126,6 @@ public class PageService {
}
}
// 자식 노드들을 재귀적으로 검색
if (currentNode.containsKey("children")) {
Map<String, Object> children = (Map<String, Object>) currentNode.get("children");
for (Map.Entry<String, Object> entry : children.entrySet()) {
@@ -116,7 +178,6 @@ public class PageService {
}
private boolean matchesQueryParams(Map<String, String> nodeParams, Map<String, String> requestParams) {
// 특별히 tab 파라미터에 대해서만 체크
String nodeTab = nodeParams.get("tab");
String requestTab = requestParams.get("tab");
+3
View File
@@ -41,3 +41,6 @@ gateway:
portal:
auth-virtual-code: 654321
test-auth-notice-enabled: true
dev:
# application.yml의 `page:` 트리(브레드크럼/메뉴 이름) 라이브 반영
hot-reload-pages: true
+3
View File
@@ -25,6 +25,9 @@ portal:
test-auth-notice-enabled: true
# 검증 단계에선 사용하지 않음
# auth-virtual-code: 654321
dev:
# application.yml의 `page:` 트리(브레드크럼/메뉴 이름) 라이브 반영
hot-reload-pages: true
ems:
+1 -8
View File
@@ -179,10 +179,6 @@ portal:
method: GET
view-name: apps/service/oauth2-guide
- path-pattern: /service/oauth2-2legged-guide
method: GET
view-name: apps/service/oauth2-2legged-guide
- path-pattern: /dashboard
method: GET
view-name: apps/mypage/dashboard
@@ -267,11 +263,8 @@ page:
name: "회원가입 안내"
path: "/service/guide"
oauth2_guide:
name: "OAuth2 인증가이드"
name: "OAuth2 개발가이드"
path: "/service/oauth2-guide"
oauth2_2legged_guide:
name: "OAuth2 2-Legged 가이드"
path: "/service/oauth2-2legged-guide"
apis:
name: "API"
path: "#"
-560
View File
@@ -18429,566 +18429,6 @@ body.commission-print-page .btn-primary:hover {
font-size: 18px;
}
}
.oauth2-guide {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #FFFFFF;
color: #1A1A2E;
font-family: "원신한", "OneShinhan", "Spoqa Han Sans Neo", sans-serif;
display: flex;
flex-direction: column;
gap: 24px;
box-sizing: border-box;
}
.oauth2-guide__eyebrow {
display: inline-block;
font-size: 14px;
font-weight: 700;
color: #0049b4;
letter-spacing: 1.2px;
margin-bottom: 8px;
}
.oauth2-guide__h2 {
margin: 0 0 12px;
font-size: 26px;
font-weight: 700;
line-height: 1.3;
color: #1A1A2E;
}
.oauth2-guide__hero {
position: relative;
display: grid;
grid-template-columns: 1fr 320px;
gap: 32px;
padding: 60px 80px;
border-radius: 20px;
border: 1px solid #E2E8F0;
background: linear-gradient(135deg, #EDF9FE 0%, #FFFFFF 100%);
}
.oauth2-guide__hero-eyebrow {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 6px 16px;
border-radius: 14px;
background: #FFFFFF;
border: 1px solid #c3dfea;
color: #0049b4;
font-size: 13px;
font-weight: 600;
margin-bottom: 20px;
}
.oauth2-guide__hero-eyebrow-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #0049b4;
}
.oauth2-guide__hero-title {
margin: 0 0 16px;
font-size: 44px;
font-weight: 800;
color: #1A1A2E;
letter-spacing: -0.5px;
line-height: 1.2;
}
.oauth2-guide__hero-lead {
margin: 0;
font-size: 17px;
font-weight: 500;
color: #64748B;
line-height: 1.6;
}
.oauth2-guide__hero-lead + .oauth2-guide__hero-lead {
margin-top: 2px;
}
.oauth2-guide__hero-chips {
display: flex;
gap: 12px;
margin-top: 24px;
flex-wrap: wrap;
}
.oauth2-guide__chip {
display: inline-flex;
align-items: center;
padding: 6px 18px;
border-radius: 15px;
background: #FFFFFF;
border: 1px solid #E2E8F0;
font-size: 13px;
font-weight: 600;
color: #1A1A2E;
}
.oauth2-guide__hero-illust {
align-self: center;
background: #FFFFFF;
border: 1px solid #c3dfea;
border-radius: 24px;
overflow: hidden;
}
.oauth2-guide__hero-illust svg {
display: block;
width: 100%;
height: auto;
}
.oauth2-guide__definition {
display: grid;
grid-template-columns: 220px 1fr;
border: 1px solid #E2E8F0;
border-radius: 16px;
background: #FFFFFF;
overflow: hidden;
}
.oauth2-guide__definition-icon {
background: #F8FAFC;
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
}
.oauth2-guide__definition-icon svg {
width: 120px;
height: 80px;
}
.oauth2-guide__definition-body {
padding: 32px 40px;
}
.oauth2-guide__definition-body .oauth2-guide__eyebrow {
margin-bottom: 6px;
}
.oauth2-guide__definition-body p {
margin: 0;
font-size: 15px;
font-weight: 500;
color: #64748B;
line-height: 1.7;
}
.oauth2-guide__definition-body p + p {
margin-top: 2px;
}
.oauth2-guide__parties-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
.oauth2-guide__party-card {
display: grid;
grid-template-columns: 88px 1fr;
gap: 16px;
align-items: center;
padding: 24px 32px;
background: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 14px;
}
.oauth2-guide__party-icon svg {
display: block;
width: 56px;
height: 44px;
}
.oauth2-guide__party-title {
margin: 0 0 8px;
font-size: 18px;
font-weight: 800;
color: #1A1A2E;
}
.oauth2-guide__party-body p {
margin: 0;
font-size: 14px;
font-weight: 500;
color: #64748B;
line-height: 1.6;
}
.oauth2-guide__party-body p + p {
margin-top: 2px;
}
.oauth2-guide__grant-sub {
margin: 0 0 20px;
font-size: 14px;
font-weight: 500;
color: #64748B;
}
.oauth2-guide__grant-sub strong {
color: #0049b4;
font-weight: 700;
}
.oauth2-guide__grant-card {
position: relative;
padding: 24px 32px;
border-radius: 14px;
background: linear-gradient(90deg, #0049b4 0%, #1f6fd0 100%);
color: #FFFFFF;
display: grid;
grid-template-columns: 1fr auto;
grid-template-areas: "badge endpoint" "title endpoint" "desc endpoint";
column-gap: 32px;
row-gap: 8px;
overflow: hidden;
}
.oauth2-guide__grant-card::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 6px;
background: #00D4FF;
}
.oauth2-guide__grant-badge {
grid-area: badge;
align-self: start;
justify-self: start;
padding: 5px 14px;
border-radius: 13px;
background: #FFFFFF;
color: #0049b4;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.5px;
}
.oauth2-guide__grant-title {
grid-area: title;
margin: 0;
font-size: 22px;
font-weight: 800;
color: #FFFFFF;
line-height: 1.3;
}
.oauth2-guide__grant-desc {
grid-area: desc;
margin: 0;
font-size: 14px;
font-weight: 500;
color: #c3dfea;
line-height: 1.6;
}
.oauth2-guide__grant-endpoint {
grid-area: endpoint;
align-self: center;
padding: 14px 18px;
background: #1A1A2E;
border-radius: 10px;
min-width: 320px;
display: flex;
flex-direction: column;
gap: 6px;
}
.oauth2-guide__endpoint-row {
display: flex;
align-items: center;
gap: 12px;
}
.oauth2-guide__method {
display: inline-block;
padding: 3px 12px;
border-radius: 4px;
background: #0049b4;
color: #FFFFFF;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.5px;
}
.oauth2-guide__path {
font-family: "Fira Code", monospace;
font-size: 13px;
color: #FFFFFF;
}
.oauth2-guide__endpoint-params {
font-family: "Fira Code", monospace;
font-size: 11px;
color: #94A3B8;
}
.oauth2-guide__sequence-diagram {
padding: 16px;
background: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 14px;
}
.oauth2-guide__sequence-diagram svg {
display: block;
width: 100%;
height: auto;
}
.oauth2-guide__bearer-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
.oauth2-guide__code-block {
position: relative;
margin: 0;
padding: 28px 24px 28px 32px;
border-radius: 14px;
background: #1A1A2E;
color: #FFFFFF;
font-family: "Fira Code", monospace;
font-size: 14px;
line-height: 1.7;
overflow-x: auto;
}
.oauth2-guide__code-block::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 6px;
background: #0049b4;
border-radius: 14px 0 0 14px;
}
.oauth2-guide__code-comment {
display: block;
color: #94A3B8;
font-size: 13px;
margin-bottom: 6px;
}
.oauth2-guide__code-line {
display: block;
color: #FFFFFF;
}
.oauth2-guide__code-line--highlight {
color: #00D4FF;
}
.oauth2-guide__lifecycle {
padding: 24px 28px;
background: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 14px;
}
.oauth2-guide__lifecycle-title {
margin: 0 0 16px;
font-size: 14px;
font-weight: 700;
color: #1A1A2E;
}
.oauth2-guide__lifecycle-steps {
list-style: none;
margin: 0 0 14px;
padding: 0;
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
}
.oauth2-guide__lifecycle-step {
display: flex;
align-items: center;
gap: 8px;
position: relative;
}
.oauth2-guide__lifecycle-step:not(:last-child)::after {
content: "";
display: inline-block;
width: 28px;
height: 0;
border-top: 1.5px dashed #94A3B8;
margin-left: 6px;
}
.oauth2-guide__lifecycle-num {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 50%;
background: #0049b4;
color: #FFFFFF;
font-size: 13px;
font-weight: 700;
}
.oauth2-guide__lifecycle-step--muted .oauth2-guide__lifecycle-num {
background: #94A3B8;
}
.oauth2-guide__lifecycle-step--muted .oauth2-guide__lifecycle-num {
background: #94A3B8;
}
.oauth2-guide__lifecycle-step--muted .oauth2-guide__lifecycle-label {
color: #64748B;
}
.oauth2-guide__lifecycle-label {
font-size: 13px;
font-weight: 600;
color: #1A1A2E;
}
.oauth2-guide__lifecycle-desc {
margin: 0;
font-size: 13px;
color: #64748B;
line-height: 1.6;
}
.oauth2-guide__lifecycle-desc + .oauth2-guide__lifecycle-desc {
margin-top: 2px;
}
.oauth2-guide__security-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20px;
}
.oauth2-guide__security-card {
padding: 24px 28px;
background: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 14px;
}
.oauth2-guide__security-icon {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 14px;
}
.oauth2-guide__security-icon svg {
width: 28px;
height: 28px;
}
.oauth2-guide__security-icon--blue {
background: #EDF9FE;
}
.oauth2-guide__security-icon--yellow {
background: #FFF9E6;
}
.oauth2-guide__security-icon--green {
background: #E8F8EE;
}
.oauth2-guide__security-title {
margin: 0 0 8px;
font-size: 15px;
font-weight: 700;
color: #1A1A2E;
}
.oauth2-guide__security-card p {
margin: 0;
font-size: 13px;
font-weight: 500;
color: #64748B;
line-height: 1.6;
}
.oauth2-guide__security-card p + p {
margin-top: 2px;
}
.oauth2-guide__cta {
position: relative;
display: block;
padding: 36px 48px;
border-radius: 20px;
background: linear-gradient(90deg, #0049b4 0%, #1f6fd0 100%);
color: #FFFFFF;
text-decoration: none;
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.oauth2-guide__cta:hover {
transform: translateY(-2px);
box-shadow: 0 12px 28px rgba(0, 73, 180, 0.25);
}
.oauth2-guide__cta-body {
position: relative;
z-index: 1;
}
.oauth2-guide__cta-eyebrow {
display: inline-block;
font-size: 13px;
font-weight: 700;
color: #c3dfea;
letter-spacing: 1.2px;
margin-bottom: 12px;
}
.oauth2-guide__cta-title {
margin: 0 0 10px;
font-size: 26px;
font-weight: 800;
color: #FFFFFF;
line-height: 1.3;
}
.oauth2-guide__cta-desc {
margin: 0 0 18px;
font-size: 14px;
font-weight: 500;
color: #c3dfea;
}
.oauth2-guide__cta-button {
display: inline-flex;
align-items: center;
padding: 10px 24px;
border-radius: 19px;
background: #FFFFFF;
color: #0049b4;
font-size: 14px;
font-weight: 700;
}
.oauth2-guide__cta-deco {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.08);
}
.oauth2-guide__cta-deco--lg {
width: 120px;
height: 120px;
right: 80px;
bottom: 30px;
}
.oauth2-guide__cta-deco--sm {
width: 80px;
height: 80px;
right: 20px;
bottom: -10px;
background: rgba(255, 255, 255, 0.1);
}
@media (max-width: 1024px) {
.oauth2-guide__hero {
grid-template-columns: 1fr;
padding: 40px;
}
.oauth2-guide__hero-illust {
max-width: 280px;
}
.oauth2-guide__definition {
grid-template-columns: 1fr;
}
.oauth2-guide__definition-icon {
padding: 24px;
}
.oauth2-guide__bearer-grid, .oauth2-guide__parties-grid {
grid-template-columns: 1fr;
}
.oauth2-guide__security-grid {
grid-template-columns: 1fr 1fr;
}
.oauth2-guide__grant-card {
grid-template-areas: "badge" "title" "desc" "endpoint";
grid-template-columns: 1fr;
}
.oauth2-guide__grant-endpoint {
min-width: 0;
}
}
@media (max-width: 640px) {
.oauth2-guide {
padding: 16px;
gap: 16px;
}
.oauth2-guide__hero {
padding: 28px 24px;
}
.oauth2-guide__hero-title {
font-size: 32px;
}
.oauth2-guide__hero-lead {
font-size: 15px;
}
.oauth2-guide__h2 {
font-size: 22px;
}
.oauth2-guide__security-grid {
grid-template-columns: 1fr;
}
.oauth2-guide__cta {
padding: 28px 24px;
}
.oauth2-guide__cta-title {
font-size: 22px;
}
}
.oauth2-2legged {
max-width: 1200px;
margin: 0 auto;
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
@@ -660,617 +660,6 @@ $service-card-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgb
}
// =============================================================================
// OAuth2 인증 가이드 (Figma 367:1054 — OAuth2 인증가이드 V2)
// =============================================================================
$oguide-primary: #0049b4;
$oguide-secondary: #c3dfea;
$oguide-bg-soft: #EDF9FE;
$oguide-bg-light: #F8FAFC;
$oguide-text-dark: #1A1A2E;
$oguide-text-gray: #64748B;
$oguide-text-mute: #94A3B8;
$oguide-border: #E2E8F0;
$oguide-accent-cyan: #00D4FF;
$oguide-accent-yellow: #FFD93D;
$oguide-accent-green: #6BCF7F;
.oauth2-guide {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #FFFFFF;
color: $oguide-text-dark;
font-family: '원신한', 'OneShinhan', 'Spoqa Han Sans Neo', sans-serif;
display: flex;
flex-direction: column;
gap: 24px;
box-sizing: border-box;
// -- common eyebrow / heading -----------------------------------------------
&__eyebrow {
display: inline-block;
font-size: 14px;
font-weight: 700;
color: $oguide-primary;
letter-spacing: 1.2px;
margin-bottom: 8px;
}
&__h2 {
margin: 0 0 12px;
font-size: 26px;
font-weight: 700;
line-height: 1.3;
color: $oguide-text-dark;
}
// -- Section 1: Hero --------------------------------------------------------
&__hero {
position: relative;
display: grid;
grid-template-columns: 1fr 320px;
gap: 32px;
padding: 60px 80px;
border-radius: 20px;
border: 1px solid $oguide-border;
background: linear-gradient(135deg, $oguide-bg-soft 0%, #FFFFFF 100%);
}
&__hero-eyebrow {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 6px 16px;
border-radius: 14px;
background: #FFFFFF;
border: 1px solid $oguide-secondary;
color: $oguide-primary;
font-size: 13px;
font-weight: 600;
margin-bottom: 20px;
}
&__hero-eyebrow-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: $oguide-primary;
}
&__hero-title {
margin: 0 0 16px;
font-size: 44px;
font-weight: 800;
color: $oguide-text-dark;
letter-spacing: -0.5px;
line-height: 1.2;
}
&__hero-lead {
margin: 0;
font-size: 17px;
font-weight: 500;
color: $oguide-text-gray;
line-height: 1.6;
& + & { margin-top: 2px; }
}
&__hero-chips {
display: flex;
gap: 12px;
margin-top: 24px;
flex-wrap: wrap;
}
&__chip {
display: inline-flex;
align-items: center;
padding: 6px 18px;
border-radius: 15px;
background: #FFFFFF;
border: 1px solid $oguide-border;
font-size: 13px;
font-weight: 600;
color: $oguide-text-dark;
}
&__hero-illust {
align-self: center;
background: #FFFFFF;
border: 1px solid $oguide-secondary;
border-radius: 24px;
overflow: hidden;
svg {
display: block;
width: 100%;
height: auto;
}
}
// -- Section 2: Definition --------------------------------------------------
&__definition {
display: grid;
grid-template-columns: 220px 1fr;
border: 1px solid $oguide-border;
border-radius: 16px;
background: #FFFFFF;
overflow: hidden;
}
&__definition-icon {
background: $oguide-bg-light;
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
svg { width: 120px; height: 80px; }
}
&__definition-body {
padding: 32px 40px;
.oauth2-guide__eyebrow { margin-bottom: 6px; }
p {
margin: 0;
font-size: 15px;
font-weight: 500;
color: $oguide-text-gray;
line-height: 1.7;
& + p { margin-top: 2px; }
}
}
// -- Section 3: Core Parties ------------------------------------------------
&__parties-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
&__party-card {
display: grid;
grid-template-columns: 88px 1fr;
gap: 16px;
align-items: center;
padding: 24px 32px;
background: #FFFFFF;
border: 1px solid $oguide-border;
border-radius: 14px;
}
&__party-icon svg {
display: block;
width: 56px;
height: 44px;
}
&__party-title {
margin: 0 0 8px;
font-size: 18px;
font-weight: 800;
color: $oguide-text-dark;
}
&__party-body p {
margin: 0;
font-size: 14px;
font-weight: 500;
color: $oguide-text-gray;
line-height: 1.6;
& + p { margin-top: 2px; }
}
// -- Section 4: Grant Type --------------------------------------------------
&__grant-sub {
margin: 0 0 20px;
font-size: 14px;
font-weight: 500;
color: $oguide-text-gray;
strong {
color: $oguide-primary;
font-weight: 700;
}
}
&__grant-card {
position: relative;
padding: 24px 32px;
border-radius: 14px;
background: linear-gradient(90deg, $oguide-primary 0%, #1f6fd0 100%);
color: #FFFFFF;
display: grid;
grid-template-columns: 1fr auto;
grid-template-areas:
"badge endpoint"
"title endpoint"
"desc endpoint";
column-gap: 32px;
row-gap: 8px;
overflow: hidden;
&::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 6px;
background: $oguide-accent-cyan;
}
}
&__grant-badge {
grid-area: badge;
align-self: start;
justify-self: start;
padding: 5px 14px;
border-radius: 13px;
background: #FFFFFF;
color: $oguide-primary;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.5px;
}
&__grant-title {
grid-area: title;
margin: 0;
font-size: 22px;
font-weight: 800;
color: #FFFFFF;
line-height: 1.3;
}
&__grant-desc {
grid-area: desc;
margin: 0;
font-size: 14px;
font-weight: 500;
color: $oguide-secondary;
line-height: 1.6;
}
&__grant-endpoint {
grid-area: endpoint;
align-self: center;
padding: 14px 18px;
background: $oguide-text-dark;
border-radius: 10px;
min-width: 320px;
display: flex;
flex-direction: column;
gap: 6px;
}
&__endpoint-row {
display: flex;
align-items: center;
gap: 12px;
}
&__method {
display: inline-block;
padding: 3px 12px;
border-radius: 4px;
background: $oguide-primary;
color: #FFFFFF;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.5px;
}
&__path {
font-family: 'Fira Code', monospace;
font-size: 13px;
color: #FFFFFF;
}
&__endpoint-params {
font-family: 'Fira Code', monospace;
font-size: 11px;
color: $oguide-text-mute;
}
// -- Section 5: Sequence Diagram --------------------------------------------
&__sequence-diagram {
padding: 16px;
background: #FFFFFF;
border: 1px solid $oguide-border;
border-radius: 14px;
svg {
display: block;
width: 100%;
height: auto;
}
}
// -- Section 6: Bearer ------------------------------------------------------
&__bearer-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
&__code-block {
position: relative;
margin: 0;
padding: 28px 24px 28px 32px;
border-radius: 14px;
background: $oguide-text-dark;
color: #FFFFFF;
font-family: 'Fira Code', monospace;
font-size: 14px;
line-height: 1.7;
overflow-x: auto;
&::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 6px;
background: $oguide-primary;
border-radius: 14px 0 0 14px;
}
}
&__code-comment {
display: block;
color: $oguide-text-mute;
font-size: 13px;
margin-bottom: 6px;
}
&__code-line {
display: block;
color: #FFFFFF;
&--highlight { color: $oguide-accent-cyan; }
}
&__lifecycle {
padding: 24px 28px;
background: #FFFFFF;
border: 1px solid $oguide-border;
border-radius: 14px;
}
&__lifecycle-title {
margin: 0 0 16px;
font-size: 14px;
font-weight: 700;
color: $oguide-text-dark;
}
&__lifecycle-steps {
list-style: none;
margin: 0 0 14px;
padding: 0;
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
}
&__lifecycle-step {
display: flex;
align-items: center;
gap: 8px;
position: relative;
&:not(:last-child)::after {
content: "";
display: inline-block;
width: 28px;
height: 0;
border-top: 1.5px dashed $oguide-text-mute;
margin-left: 6px;
}
}
&__lifecycle-num {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 50%;
background: $oguide-primary;
color: #FFFFFF;
font-size: 13px;
font-weight: 700;
}
&__lifecycle-step--muted &__lifecycle-num { background: $oguide-text-mute; }
// ↑ doesn't work in nesting; explicit modifier below:
&__lifecycle-step--muted .oauth2-guide__lifecycle-num { background: $oguide-text-mute; }
&__lifecycle-step--muted .oauth2-guide__lifecycle-label { color: $oguide-text-gray; }
&__lifecycle-label {
font-size: 13px;
font-weight: 600;
color: $oguide-text-dark;
}
&__lifecycle-desc {
margin: 0;
font-size: 13px;
color: $oguide-text-gray;
line-height: 1.6;
& + & { margin-top: 2px; }
}
// -- Section 7: Security ----------------------------------------------------
&__security-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20px;
}
&__security-card {
padding: 24px 28px;
background: #FFFFFF;
border: 1px solid $oguide-border;
border-radius: 14px;
}
&__security-icon {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 14px;
svg { width: 28px; height: 28px; }
&--blue { background: $oguide-bg-soft; }
&--yellow { background: #FFF9E6; }
&--green { background: #E8F8EE; }
}
&__security-title {
margin: 0 0 8px;
font-size: 15px;
font-weight: 700;
color: $oguide-text-dark;
}
&__security-card p {
margin: 0;
font-size: 13px;
font-weight: 500;
color: $oguide-text-gray;
line-height: 1.6;
& + p { margin-top: 2px; }
}
// -- Section 8: CTA ---------------------------------------------------------
&__cta {
position: relative;
display: block;
padding: 36px 48px;
border-radius: 20px;
background: linear-gradient(90deg, $oguide-primary 0%, #1f6fd0 100%);
color: #FFFFFF;
text-decoration: none;
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 12px 28px rgba(0, 73, 180, 0.25);
}
}
&__cta-body { position: relative; z-index: 1; }
&__cta-eyebrow {
display: inline-block;
font-size: 13px;
font-weight: 700;
color: $oguide-secondary;
letter-spacing: 1.2px;
margin-bottom: 12px;
}
&__cta-title {
margin: 0 0 10px;
font-size: 26px;
font-weight: 800;
color: #FFFFFF;
line-height: 1.3;
}
&__cta-desc {
margin: 0 0 18px;
font-size: 14px;
font-weight: 500;
color: $oguide-secondary;
}
&__cta-button {
display: inline-flex;
align-items: center;
padding: 10px 24px;
border-radius: 19px;
background: #FFFFFF;
color: $oguide-primary;
font-size: 14px;
font-weight: 700;
}
&__cta-deco {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.08);
&--lg {
width: 120px;
height: 120px;
right: 80px;
bottom: 30px;
}
&--sm {
width: 80px;
height: 80px;
right: 20px;
bottom: -10px;
background: rgba(255, 255, 255, 0.10);
}
}
}
@media (max-width: 1024px) {
.oauth2-guide {
&__hero {
grid-template-columns: 1fr;
padding: 40px;
}
&__hero-illust { max-width: 280px; }
&__definition { grid-template-columns: 1fr; }
&__definition-icon { padding: 24px; }
&__bearer-grid,
&__parties-grid { grid-template-columns: 1fr; }
&__security-grid { grid-template-columns: 1fr 1fr; }
&__grant-card {
grid-template-areas:
"badge"
"title"
"desc"
"endpoint";
grid-template-columns: 1fr;
}
&__grant-endpoint { min-width: 0; }
}
}
@media (max-width: 640px) {
.oauth2-guide {
padding: 16px;
gap: 16px;
&__hero { padding: 28px 24px; }
&__hero-title { font-size: 32px; }
&__hero-lead { font-size: 15px; }
&__h2 { font-size: 22px; }
&__security-grid { grid-template-columns: 1fr; }
&__cta { padding: 28px 24px; }
&__cta-title { font-size: 22px; }
}
}
// =============================================================================
// OAuth2 2-Legged 구현 가이드 (Figma 367:1564 - OAuth2 2-Legged 가이드 V3)
@@ -1,422 +0,0 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/djbank_title_layout}">
<body>
<th:block layout:fragment="contentFragment">
<section class="oauth2-2legged">
<!-- Section 1: Hero -->
<header class="oauth2-2legged__hero">
<div class="oauth2-2legged__hero-body">
<span class="oauth2-2legged__hero-eyebrow">
<span class="oauth2-2legged__hero-eyebrow-dot"></span>
개발 가이드 · 2-Legged · Client Credentials
</span>
<h1 class="oauth2-2legged__hero-title">OAuth 2.0 · 2-Legged 구현 가이드</h1>
<p class="oauth2-2legged__hero-lead">DJBank Open API를 호출하기 위한 ClientID/Secret 기반 토큰 발급과</p>
<p class="oauth2-2legged__hero-lead">Bearer 인증 호출 방법을 단계별로 설명합니다.</p>
<div class="oauth2-2legged__hero-chips">
<span class="oauth2-2legged__chip oauth2-2legged__chip--primary">Server-to-Server</span>
<span class="oauth2-2legged__chip">cURL 예제</span>
<span class="oauth2-2legged__chip">application/x-www-form</span>
</div>
</div>
<div class="oauth2-2legged__hero-illust" aria-hidden="true">
<svg viewBox="0 0 280 200" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Server-to-Server Token Flow">
<defs>
<marker id="o2leg-hero-arrow" 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>
</defs>
<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"/>
<text x="80" y="120" text-anchor="middle" font-size="11" font-weight="700" fill="#0049b4">Client</text>
<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>
</header>
<!-- Section 2: 사전 준비 -->
<section class="oauth2-2legged__prereq" aria-labelledby="o2leg-prereq-title">
<span class="oauth2-2legged__eyebrow">PREREQUISITE</span>
<h2 class="oauth2-2legged__h2" id="o2leg-prereq-title">시작 전 준비 사항</h2>
<div class="oauth2-2legged__prereq-grid">
<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">
<span class="oauth2-2legged__prereq-num">2</span>
<div class="oauth2-2legged__prereq-body">
<h3 class="oauth2-2legged__prereq-title">ClientID / Secret 발급</h3>
<p>승인 완료 후 발급된 자격증명을</p>
<p>서버 환경변수에 안전하게 보관.</p>
</div>
</article>
<article class="oauth2-2legged__prereq-card">
<span class="oauth2-2legged__prereq-num">3</span>
<div class="oauth2-2legged__prereq-body">
<h3 class="oauth2-2legged__prereq-title">Scope 확인</h3>
<p>호출하려는 API에 필요한 scope</p>
<p>권한이 부여됐는지 확인합니다.</p>
</div>
</article>
</div>
</section>
<!-- Section 3: 전체 인증·호출 시퀀스 -->
<section class="oauth2-2legged__sequence" aria-labelledby="o2leg-seq-title">
<span class="oauth2-2legged__eyebrow">SEQUENCE</span>
<h2 class="oauth2-2legged__h2" id="o2leg-seq-title">전체 인증·호출 시퀀스</h2>
<div class="oauth2-2legged__sequence-diagram">
<svg viewBox="0 0 1120 282" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="OAuth 2.0 2-Legged Sequence">
<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>
<g>
<rect x="120" y="24" width="240" height="48" rx="24" fill="#EDF9FE" stroke="#0049b4"/>
<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="102" text-anchor="middle" font-size="12" font-weight="700" fill="#0049b4">① POST /dj/oauth/token</text>
<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="160" text-anchor="middle" font-size="12" font-weight="700" fill="#64748B">② access_token 발급</text>
<line x1="880" y1="170" x2="240" y2="170" stroke="#64748B" stroke-width="2" marker-end="url(#o2leg-arrow-gray)"/>
<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="560" y="216" text-anchor="middle" font-size="12" font-weight="700" fill="#0049b4">③ GET /api/v1/... · Authorization: Bearer &lt;access_token&gt;</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="248" text-anchor="middle" font-size="12" font-weight="700" fill="#64748B">④ 검증 후 API 응답</text>
<line x1="880" y1="254" x2="240" y2="254" stroke="#64748B" stroke-width="2" marker-end="url(#o2leg-arrow-gray)"/>
</svg>
</div>
</section>
<!-- Section 4: STEP 1 — 액세스 토큰 요청 -->
<section class="oauth2-2legged__step" aria-labelledby="o2leg-step1-title">
<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">
<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>
<th>PARAMETER</th>
<th>REQ</th>
<th>설명</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>grant_type</code></td>
<td><span class="oauth2-2legged__req-badge oauth2-2legged__req-badge--required">필수</span></td>
<td>고정값 "client_credentials"</td>
</tr>
<tr>
<td><code>client_id</code></td>
<td><span class="oauth2-2legged__req-badge oauth2-2legged__req-badge--required">필수</span></td>
<td>발급받은 클라이언트 식별자</td>
</tr>
<tr>
<td><code>client_secret</code></td>
<td><span class="oauth2-2legged__req-badge oauth2-2legged__req-badge--required">필수</span></td>
<td>발급받은 비밀 키 (서버 보관)</td>
</tr>
<tr>
<td><code>scope</code></td>
<td><span class="oauth2-2legged__req-badge oauth2-2legged__req-badge--optional">선택</span></td>
<td>호출 권한 범위 (공백 구분)</td>
</tr>
</tbody>
</table>
<p class="oauth2-2legged__warning">⚠ Content-Type: application/x-www-form-urlencoded 헤더 필수</p>
</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"># 액세스 토큰 발급</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> \
-d <span class="o2leg-c">'grant_type=client_credentials'</span> \
-d <span class="o2leg-c">'client_id=YOUR_CLIENT_ID'</span> \
-d <span class="o2leg-c">'client_secret=YOUR_CLIENT_SECRET'</span> \
-d <span class="o2leg-c">'scope=read.accounts'</span>
<span class="o2leg-g"># 응답: 200 OK + JSON</span></pre>
</div>
</div>
</section>
<!-- Section 5: STEP 2 — 액세스 토큰 응답 -->
<section class="oauth2-2legged__step" aria-labelledby="o2leg-step2-title">
<span class="oauth2-2legged__eyebrow">STEP 2</span>
<h2 class="oauth2-2legged__h2" id="o2leg-step2-title">액세스 토큰 응답</h2>
<p class="oauth2-2legged__desc">정상 발급 시 200 OK 와 함께 다음 JSON 을 반환합니다.</p>
<div class="oauth2-2legged__step-grid">
<div class="oauth2-2legged__code-panel">
<span class="oauth2-2legged__code-tag oauth2-2legged__code-tag--ok">200 OK · JSON</span>
<pre class="oauth2-2legged__code-block">{
<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">"expires_in"</span>: <span class="o2leg-p">86400</span>,
<span class="o2leg-c">"scope"</span>: <span class="o2leg-y">"read.accounts"</span>,
<span class="o2leg-c">"jti"</span>: <span class="o2leg-y">"f47ac10b-58cc-4372-..."</span>
}
<span class="o2leg-g"># 응답 본문은 캐싱하여 expires_in 동안 재사용 가능</span></pre>
</div>
<div class="oauth2-2legged__panel">
<h3 class="oauth2-2legged__panel-title">응답 필드</h3>
<table class="oauth2-2legged__table">
<thead>
<tr>
<th>FIELD</th>
<th>TYPE</th>
<th>설명</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="oauth2-2legged__field-name">access_token</code></td>
<td>string</td>
<td>Bearer 호출에 사용할 토큰</td>
</tr>
<tr>
<td><code class="oauth2-2legged__field-name">token_type</code></td>
<td>string</td>
<td>고정값 "bearer"</td>
</tr>
<tr>
<td><code class="oauth2-2legged__field-name">expires_in</code></td>
<td>int</td>
<td>유효 시간(초). 기본 86400</td>
</tr>
<tr>
<td><code class="oauth2-2legged__field-name">scope</code></td>
<td>string</td>
<td>실제 부여된 권한 범위</td>
</tr>
<tr>
<td><code class="oauth2-2legged__field-name">jti</code></td>
<td>string</td>
<td>토큰 고유 ID (감사/취소용)</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- Section 6: STEP 3 — API 호출 -->
<section class="oauth2-2legged__step" aria-labelledby="o2leg-step3-title">
<span class="oauth2-2legged__eyebrow">STEP 3</span>
<h2 class="oauth2-2legged__h2" id="o2leg-step3-title">발급 토큰으로 API 호출</h2>
<p class="oauth2-2legged__desc">Authorization 헤더에 Bearer 토큰을 실어 보호 자원 API 를 호출합니다.</p>
<div class="oauth2-2legged__step-grid">
<div class="oauth2-2legged__panel">
<h3 class="oauth2-2legged__panel-title">필수 헤더</h3>
<div class="oauth2-2legged__header-box">
<code class="oauth2-2legged__header-key">Authorization:</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 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> \
<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">'Accept: application/json'</span>
<span class="o2leg-g"># 응답</span>
HTTP/1.1 <span class="o2leg-g">200 OK</span>
{ <span class="o2leg-c">"accounts"</span>: [ ... ] }</pre>
</div>
</div>
</section>
<!-- Section 7: 대표 에러 응답 (표) -->
<section class="oauth2-2legged__errors" aria-labelledby="o2leg-errors-title">
<span class="oauth2-2legged__eyebrow">ERROR CODES</span>
<h2 class="oauth2-2legged__h2" id="o2leg-errors-title">대표 에러 응답</h2>
<div class="oauth2-2legged__panel">
<table class="oauth2-2legged__table oauth2-2legged__table--errors">
<thead>
<tr>
<th>HTTP</th>
<th>ERROR CODE</th>
<th>의미</th>
<th>해결 가이드</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="oauth2-2legged__status-badge oauth2-2legged__status-badge--400">400</span></td>
<td><code>invalid_request</code></td>
<td>필수 파라미터 누락/형식 오류</td>
<td>grant_type, client_id, client_secret 확인</td>
</tr>
<tr>
<td><span class="oauth2-2legged__status-badge oauth2-2legged__status-badge--400">401</span></td>
<td><code>invalid_client</code></td>
<td>자격증명 불일치 / 미승인</td>
<td>ClientID·Secret 재확인, 앱 상태 점검</td>
</tr>
<tr>
<td><span class="oauth2-2legged__status-badge oauth2-2legged__status-badge--400">401</span></td>
<td><code>invalid_token</code></td>
<td>만료/위·변조된 토큰</td>
<td>토큰 재발급 후 재시도</td>
</tr>
<tr>
<td><span class="oauth2-2legged__status-badge oauth2-2legged__status-badge--403">403</span></td>
<td><code>insufficient_scope</code></td>
<td>scope 권한 부족</td>
<td>필요 scope 추가 신청 후 재발급</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- Section 8: 에러 응답 예시 (단일 사례) -->
<section class="oauth2-2legged__error-example" aria-labelledby="o2leg-err-ex-title">
<span class="oauth2-2legged__eyebrow">ERROR RESPONSE</span>
<h2 class="oauth2-2legged__h2" id="o2leg-err-ex-title">에러 응답 예시</h2>
<p class="oauth2-2legged__desc">자격증명 불일치 시 401 Unauthorized 와 함께 다음 JSON 을 반환합니다.</p>
<div class="oauth2-2legged__step-grid">
<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>
<pre class="oauth2-2legged__code-block">{
<span class="o2leg-c">"error"</span>: <span class="o2leg-y">"invalid_client"</span>,
<span class="o2leg-c">"error_description"</span>:
<span class="o2leg-y">"Client authentication failed"</span>,
<span class="o2leg-c">"status"</span>: <span class="o2leg-p">401</span>,
<span class="o2leg-c">"timestamp"</span>: <span class="o2leg-y">"2026-06-10T11:23:45Z"</span>
}
<span class="o2leg-g"># 처리: ClientID·Secret 확인 후 재시도</span></pre>
</div>
<div class="oauth2-2legged__panel">
<h3 class="oauth2-2legged__panel-title">응답 필드</h3>
<table class="oauth2-2legged__table">
<thead>
<tr>
<th>FIELD</th>
<th>TYPE</th>
<th>설명</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="oauth2-2legged__field-name">error</code></td>
<td>string</td>
<td>에러 코드 (예: invalid_client)</td>
</tr>
<tr>
<td><code class="oauth2-2legged__field-name">error_description</code></td>
<td>string</td>
<td>사람이 읽을 수 있는 설명</td>
</tr>
<tr>
<td><code class="oauth2-2legged__field-name">status</code></td>
<td>int</td>
<td>HTTP 상태 코드</td>
</tr>
<tr>
<td><code class="oauth2-2legged__field-name">timestamp</code></td>
<td>string</td>
<td>에러 발생 시각 (ISO 8601)</td>
</tr>
</tbody>
</table>
<p class="oauth2-2legged__error-note">⚠ error 코드는 RFC 6749 표준 코드 또는 DJBank 확장 코드</p>
</div>
</div>
</section>
<!-- Section 9: API 목록 CTA -->
<a class="oauth2-2legged__cta" th:href="@{/apis}">
<div class="oauth2-2legged__cta-body">
<span class="oauth2-2legged__cta-eyebrow">EXPLORE</span>
<h2 class="oauth2-2legged__cta-title">API 목록 보러가기</h2>
<p class="oauth2-2legged__cta-desc">사용 가능한 DJBank Open API 카탈로그를 확인하세요.</p>
<span class="oauth2-2legged__cta-button">API 목록 →</span>
</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--sm" aria-hidden="true"></span>
</a>
</section>
</th:block>
</body>
<th:block layout:fragment="contentScript">
</th:block>
</html>
@@ -5,260 +5,410 @@
<body>
<th:block layout:fragment="contentFragment">
<section class="oauth2-guide">
<section class="oauth2-2legged">
<!-- Section 1: Hero -->
<header class="oauth2-guide__hero">
<div class="oauth2-guide__hero-body">
<span class="oauth2-guide__hero-eyebrow">
<span class="oauth2-guide__hero-eyebrow-dot"></span>
개발 가이드 · Authentication
<header class="oauth2-2legged__hero">
<div class="oauth2-2legged__hero-body">
<span class="oauth2-2legged__hero-eyebrow">
<span class="oauth2-2legged__hero-eyebrow-dot"></span>
개발 가이드 · 2-Legged · Client Credentials
</span>
<h1 class="oauth2-guide__hero-title">OAuth 2.0 인증 개요</h1>
<p class="oauth2-guide__hero-lead">DJBank Open API를 사용하기 위한 표준 인증 방식,</p>
<p class="oauth2-guide__hero-lead">OAuth 2.0의 개념과 동작 원리를 한눈에 정리했습니다.</p>
<div class="oauth2-guide__hero-chips">
<span class="oauth2-guide__chip">2-Legged</span>
<span class="oauth2-guide__chip">Client Credentials</span>
<span class="oauth2-guide__chip">Bearer Token</span>
<h1 class="oauth2-2legged__hero-title">OAuth2 개발가이드</h1>
<p class="oauth2-2legged__hero-lead">DJBank Open API를 호출하기 위한 ClientID/Secret 기반 토큰 발급과</p>
<p class="oauth2-2legged__hero-lead">Bearer 인증 호출 방법을 단계별로 설명합니다.</p>
<div class="oauth2-2legged__hero-chips">
<span class="oauth2-2legged__chip oauth2-2legged__chip--primary">Server-to-Server</span>
<span class="oauth2-2legged__chip">cURL 예제</span>
<span class="oauth2-2legged__chip">application/x-www-form</span>
</div>
</div>
<div class="oauth2-guide__hero-illust" aria-hidden="true">
<svg viewBox="0 0 280 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="OAuth Shield Illustration">
<div class="oauth2-2legged__hero-illust" aria-hidden="true">
<svg viewBox="0 0 280 200" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Server-to-Server Token Flow">
<defs>
<linearGradient id="oguide-shield" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0049b4"/>
<stop offset="100%" stop-color="#0066d6"/>
</linearGradient>
<marker id="o2leg-hero-arrow" 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>
</defs>
<rect x="0" y="0" width="280" height="220" rx="24" fill="#FFFFFF"/>
<circle cx="40" cy="40" r="3" fill="#00D4FF"/>
<circle cx="60" cy="32" r="2" fill="#A78BFA"/>
<circle cx="240" cy="190" r="3" fill="#FFD93D"/>
<circle cx="220" cy="200" r="2" fill="#6BCF7F"/>
<g transform="translate(80,40)">
<path d="M60 0 L115 22 L115 70 C115 105 88 130 60 140 C32 130 5 105 5 70 L5 22 Z" fill="url(#oguide-shield)"/>
<path d="M30 70 L52 92 L92 50" stroke="#FFFFFF" stroke-width="6" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<g transform="translate(30,170)">
<rect width="118" height="34" rx="6" fill="#1A1A2E"/>
<text x="14" y="22" font-family="'Fira Code', monospace" font-size="12" fill="#00D4FF">access_token</text>
<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"/>
<text x="80" y="120" text-anchor="middle" font-size="11" font-weight="700" fill="#0049b4">Client</text>
<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>
</header>
<!-- Section 2: Definition -->
<section class="oauth2-guide__definition" aria-labelledby="oguide-def-title">
<div class="oauth2-guide__definition-icon" aria-hidden="true">
<svg viewBox="0 0 120 80" xmlns="http://www.w3.org/2000/svg">
<circle cx="40" cy="40" r="22" fill="none" stroke="#0049b4" stroke-width="6"/>
<path d="M62 40 L110 40 L110 56 M94 40 L94 56" stroke="#0049b4" stroke-width="6" fill="none" stroke-linecap="round"/>
<circle cx="40" cy="40" r="6" fill="#0049b4"/>
</svg>
</div>
<div class="oauth2-guide__definition-body">
<span class="oauth2-guide__eyebrow">DEFINITION</span>
<h2 class="oauth2-guide__h2" id="oguide-def-title">OAuth 2.0이란?</h2>
<p>사용자의 비밀번호를 직접 공유하지 않고, 안전하게 자원 접근 권한을</p>
<p>위임받기 위한 산업 표준 인증·인가 프로토콜(RFC 6749)입니다.</p>
<p>발급된 액세스 토큰(Bearer Token)으로 API를 호출합니다.</p>
</div>
</section>
<!-- Section 2: 사전 준비 -->
<section class="oauth2-2legged__prereq" aria-labelledby="o2leg-prereq-title">
<span class="oauth2-2legged__eyebrow">PREREQUISITE</span>
<h2 class="oauth2-2legged__h2" id="o2leg-prereq-title">시작 전 준비 사항</h2>
<!-- Section 3: Core Parties -->
<section class="oauth2-guide__parties" aria-labelledby="oguide-parties-title">
<span class="oauth2-guide__eyebrow">CORE PARTIES</span>
<h2 class="oauth2-guide__h2" id="oguide-parties-title">OAuth 2.0 주요 구성</h2>
<div class="oauth2-guide__parties-grid">
<article class="oauth2-guide__party-card">
<div class="oauth2-guide__party-icon" aria-hidden="true">
<svg viewBox="0 0 56 44" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="56" height="44" rx="8" fill="#EDF9FE" stroke="#0049b4" stroke-width="2"/>
<line x1="0" y1="14" x2="56" y2="14" stroke="#0049b4" stroke-width="2"/>
<circle cx="10" cy="7" r="2" fill="#0049b4"/>
<circle cx="18" cy="7" r="2" fill="#0049b4"/>
<rect x="10" y="22" width="36" height="4" rx="2" fill="#0049b4" opacity="0.4"/>
<rect x="10" y="32" width="24" height="4" rx="2" fill="#0049b4" opacity="0.4"/>
</svg>
</div>
<div class="oauth2-guide__party-body">
<h3 class="oauth2-guide__party-title">Client</h3>
<p>API를 호출하는 애플리케이션.</p>
<p>기업 백엔드·배치·시스템 등.</p>
<div class="oauth2-2legged__prereq-grid">
<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-guide__party-card">
<div class="oauth2-guide__party-icon" aria-hidden="true">
<svg viewBox="0 0 56 44" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="oguide-party-shield" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0049b4"/>
<stop offset="100%" stop-color="#0066d6"/>
</linearGradient>
</defs>
<rect x="0" y="0" width="56" height="44" rx="8" fill="#FFFFFF" stroke="#c3dfea"/>
<path d="M28 6 L46 14 L46 30 C46 36 36 40 28 40 C20 40 10 36 10 30 L10 14 Z" fill="url(#oguide-party-shield)"/>
<path d="M20 24 L26 30 L36 18" stroke="#FFFFFF" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<article class="oauth2-2legged__prereq-card">
<span class="oauth2-2legged__prereq-num">2</span>
<div class="oauth2-2legged__prereq-body">
<h3 class="oauth2-2legged__prereq-title">ClientID / Secret 발급</h3>
<p>승인 완료 후 발급된 자격증명을</p>
<p>서버 환경변수에 안전하게 보관.</p>
</div>
<div class="oauth2-guide__party-body">
<h3 class="oauth2-guide__party-title">DJBank Open API</h3>
<p>토큰 발급과 API 응답을 모두 담당하는</p>
<p>DJBank의 단일 진입점입니다.</p>
</article>
<article class="oauth2-2legged__prereq-card">
<span class="oauth2-2legged__prereq-num">3</span>
<div class="oauth2-2legged__prereq-body">
<h3 class="oauth2-2legged__prereq-title">Scope 확인</h3>
<p>호출하려는 API에 필요한 scope</p>
<p>권한이 부여됐는지 확인합니다.</p>
</div>
</article>
</div>
</section>
<!-- Section 4: Grant Type -->
<section class="oauth2-guide__grant" aria-labelledby="oguide-grant-title">
<span class="oauth2-guide__eyebrow">GRANT TYPE</span>
<h2 class="oauth2-guide__h2" id="oguide-grant-title">지원 Grant Type</h2>
<p class="oauth2-guide__grant-sub">
DJBank는 <strong>Client Credentials (2-Legged)</strong> 방식을 지원합니다.
</p>
<!-- Section 3: 전체 인증·호출 시퀀스 -->
<section class="oauth2-2legged__sequence" aria-labelledby="o2leg-seq-title">
<span class="oauth2-2legged__eyebrow">SEQUENCE</span>
<h2 class="oauth2-2legged__h2" id="o2leg-seq-title">전체 인증·호출 시퀀스</h2>
<article class="oauth2-guide__grant-card">
<span class="oauth2-guide__grant-badge">SUPPORTED</span>
<h3 class="oauth2-guide__grant-title">Client Credentials (2-Legged)</h3>
<p class="oauth2-guide__grant-desc">
서버 간 통신 / 시스템 호출에 사용. ClientID/Secret만으로 액세스 토큰을 발급받습니다.
</p>
<div class="oauth2-guide__grant-endpoint">
<div class="oauth2-guide__endpoint-row">
<span class="oauth2-guide__method">POST</span>
<code class="oauth2-guide__path">/dj/oauth/token</code>
</div>
<code class="oauth2-guide__endpoint-params">grant_type=client_credentials</code>
</div>
</article>
</section>
<!-- Section 5: How it works (sequence) -->
<section class="oauth2-guide__sequence" aria-labelledby="oguide-seq-title">
<span class="oauth2-guide__eyebrow">HOW IT WORKS</span>
<h2 class="oauth2-guide__h2" id="oguide-seq-title">기본 동작 흐름</h2>
<div class="oauth2-guide__sequence-diagram">
<svg viewBox="0 0 1120 216" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="OAuth 2.0 Sequence Diagram">
<div class="oauth2-2legged__sequence-diagram">
<svg viewBox="0 0 1120 282" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="OAuth 2.0 2-Legged Sequence">
<defs>
<marker id="oguide-arrow-primary" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto">
<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="oguide-arrow-gray" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto">
<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>
<g>
<rect x="160" y="20" width="200" height="44" rx="22" fill="#EDF9FE" stroke="#0049b4"/>
<text x="260" y="48" text-anchor="middle" font-size="14" font-weight="700" fill="#0049b4">Client (앱)</text>
<line x1="260" y1="64" x2="260" y2="200" stroke="#94A3B8" stroke-dasharray="4 4"/>
<rect x="120" y="24" width="240" height="48" rx="24" fill="#EDF9FE" stroke="#0049b4"/>
<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="20" width="200" height="44" rx="22" fill="#FFFFFF" stroke="#0049b4"/>
<text x="860" y="48" text-anchor="middle" font-size="14" font-weight="700" fill="#0049b4">DJBank Open API</text>
<line x1="860" y1="64" x2="860" y2="200" stroke="#94A3B8" stroke-dasharray="4 4"/>
<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="90" text-anchor="middle" font-size="12" font-weight="700" fill="#0049b4">ClientID + Secret 으로 토큰 요청</text>
<line x1="260" y1="100" x2="860" y2="100" stroke="#0049b4" stroke-width="2" marker-end="url(#oguide-arrow-primary)"/>
<text x="560" y="102" text-anchor="middle" font-size="12" font-weight="700" fill="#0049b4">POST /dj/oauth/token</text>
<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="124" text-anchor="middle" font-size="12" font-weight="700" fill="#64748B">② access_token 발급</text>
<line x1="860" y1="134" x2="260" y2="134" stroke="#64748B" stroke-width="2" marker-end="url(#oguide-arrow-gray)"/>
<text x="560" y="160" text-anchor="middle" font-size="12" font-weight="700" fill="#64748B">② access_token 발급</text>
<line x1="880" y1="170" x2="240" y2="170" stroke="#64748B" stroke-width="2" marker-end="url(#o2leg-arrow-gray)"/>
<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="560" y="162" text-anchor="middle" font-size="12" font-weight="700" fill="#0049b4">③ Authorization: Bearer ... 헤더로 API 호출</text>
<line x1="260" y1="172" x2="860" y2="172" stroke="#0049b4" stroke-width="2" marker-end="url(#oguide-arrow-primary)"/>
<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>
<line x1="240" y1="226" x2="880" y2="226" stroke="#0049b4" stroke-width="2" marker-end="url(#o2leg-arrow-primary)"/>
<text x="560" y="190" text-anchor="middle" font-size="12" font-weight="700" fill="#64748B">④ 검증 후 API 응답</text>
<line x1="860" y1="200" x2="260" y2="200" stroke="#64748B" stroke-width="2" marker-end="url(#oguide-arrow-gray)"/>
<text x="560" y="248" text-anchor="middle" font-size="12" font-weight="700" fill="#64748B">④ 검증 후 API 응답</text>
<line x1="880" y1="254" x2="240" y2="254" stroke="#64748B" stroke-width="2" marker-end="url(#o2leg-arrow-gray)"/>
</svg>
</div>
</section>
<!-- Section 6: Bearer Token -->
<section class="oauth2-guide__bearer" aria-labelledby="oguide-bearer-title">
<span class="oauth2-guide__eyebrow">BEARER TOKEN</span>
<h2 class="oauth2-guide__h2" id="oguide-bearer-title">발급된 토큰으로 API 호출</h2>
<!-- Section 4: STEP 1 — 액세스 토큰 요청 -->
<section class="oauth2-2legged__step" aria-labelledby="o2leg-step1-title">
<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-guide__bearer-grid">
<pre class="oauth2-guide__code-block"><span class="oauth2-guide__code-comment">// HTTP Request Header</span>
<span class="oauth2-guide__code-line">GET /api/v1/accounts HTTP/1.1</span>
<span class="oauth2-guide__code-line">Host: openapi.djbank.co.kr</span>
<span class="oauth2-guide__code-line oauth2-guide__code-line--highlight">Authorization: Bearer eyJhbGciOi...</span></pre>
<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-guide__lifecycle">
<h3 class="oauth2-guide__lifecycle-title">토큰 라이프사이클</h3>
<ol class="oauth2-guide__lifecycle-steps">
<li class="oauth2-guide__lifecycle-step"><span class="oauth2-guide__lifecycle-num">1</span><span class="oauth2-guide__lifecycle-label">발급</span></li>
<li class="oauth2-guide__lifecycle-step"><span class="oauth2-guide__lifecycle-num">2</span><span class="oauth2-guide__lifecycle-label">사용</span></li>
<li class="oauth2-guide__lifecycle-step"><span class="oauth2-guide__lifecycle-num">3</span><span class="oauth2-guide__lifecycle-label">만료</span></li>
<li class="oauth2-guide__lifecycle-step oauth2-guide__lifecycle-step--muted"><span class="oauth2-guide__lifecycle-num">4</span><span class="oauth2-guide__lifecycle-label">재발급</span></li>
</ol>
<p class="oauth2-guide__lifecycle-desc">TTL 내에는 토큰을 재사용하고,</p>
<p class="oauth2-guide__lifecycle-desc">만료 시 새 토큰을 발급받아 호출합니다.</p>
<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>
<th>PARAMETER</th>
<th>REQ</th>
<th>설명</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>grant_type</code></td>
<td><span class="oauth2-2legged__req-badge oauth2-2legged__req-badge--required">필수</span></td>
<td>고정값 "client_credentials"</td>
</tr>
<tr>
<td><code>client_id</code></td>
<td><span class="oauth2-2legged__req-badge oauth2-2legged__req-badge--required">필수</span></td>
<td>발급받은 클라이언트 식별자</td>
</tr>
<tr>
<td><code>client_secret</code></td>
<td><span class="oauth2-2legged__req-badge oauth2-2legged__req-badge--required">필수</span></td>
<td>발급받은 비밀 키 (서버 보관)</td>
</tr>
<tr>
<td><code>scope</code></td>
<td><span class="oauth2-2legged__req-badge oauth2-2legged__req-badge--optional">선택</span></td>
<td>호출 권한 범위 (공백 구분)</td>
</tr>
</tbody>
</table>
<p class="oauth2-2legged__warning">⚠ Content-Type: application/x-www-form-urlencoded 헤더 필수</p>
</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"># 액세스 토큰 발급</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> \
-d <span class="o2leg-c">'grant_type=client_credentials'</span> \
-d <span class="o2leg-c">'client_id=YOUR_CLIENT_ID'</span> \
-d <span class="o2leg-c">'client_secret=YOUR_CLIENT_SECRET'</span> \
-d <span class="o2leg-c">'scope=read.accounts'</span>
<span class="o2leg-g"># 응답: 200 OK + JSON</span></pre>
</div>
</div>
</section>
<!-- Section 7: Security -->
<section class="oauth2-guide__security" aria-labelledby="oguide-security-title">
<span class="oauth2-guide__eyebrow">SECURITY</span>
<h2 class="oauth2-guide__h2" id="oguide-security-title">기본 보안 권고</h2>
<!-- Section 5: STEP 2 — 액세스 토큰 응답 -->
<section class="oauth2-2legged__step" aria-labelledby="o2leg-step2-title">
<span class="oauth2-2legged__eyebrow">STEP 2</span>
<h2 class="oauth2-2legged__h2" id="o2leg-step2-title">액세스 토큰 응답</h2>
<p class="oauth2-2legged__desc">정상 발급 시 200 OK 와 함께 다음 JSON 을 반환합니다.</p>
<div class="oauth2-guide__security-grid">
<article class="oauth2-guide__security-card">
<div class="oauth2-guide__security-icon oauth2-guide__security-icon--blue" aria-hidden="true">
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path d="M16 4 L24 8 L24 18 C24 24 16 28 16 28 C16 28 8 24 8 18 L8 8 Z" fill="#0049b4"/>
</svg>
</div>
<h3 class="oauth2-guide__security-title">HTTPS 필수</h3>
<p>모든 토큰·API 호출은</p>
<p>반드시 TLS 위에서만 수행합니다.</p>
</article>
<div class="oauth2-2legged__step-grid">
<div class="oauth2-2legged__code-panel">
<span class="oauth2-2legged__code-tag oauth2-2legged__code-tag--ok">200 OK · JSON</span>
<pre class="oauth2-2legged__code-block">{
<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">"expires_in"</span>: <span class="o2leg-p">86400</span>,
<span class="o2leg-c">"scope"</span>: <span class="o2leg-y">"read.accounts"</span>,
<span class="o2leg-c">"jti"</span>: <span class="o2leg-y">"f47ac10b-58cc-4372-..."</span>
}
<article class="oauth2-guide__security-card">
<div class="oauth2-guide__security-icon oauth2-guide__security-icon--yellow" aria-hidden="true">
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<rect x="8" y="14" width="16" height="14" rx="2" fill="#FFD93D"/>
<path d="M11 14 L11 9 C11 6 13 4 16 4 C19 4 21 6 21 9 L21 14" stroke="#FFD93D" stroke-width="2.5" fill="none"/>
</svg>
</div>
<h3 class="oauth2-guide__security-title">Secret 서버 보관</h3>
<p>client_secret 은 클라이언트 측</p>
<p>코드·브라우저에 절대 노출 금지.</p>
</article>
<span class="o2leg-g"># 응답 본문은 캐싱하여 expires_in 동안 재사용 가능</span></pre>
</div>
<article class="oauth2-guide__security-card">
<div class="oauth2-guide__security-icon oauth2-guide__security-icon--green" aria-hidden="true">
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<circle cx="16" cy="16" r="9" fill="none" stroke="#6BCF7F" stroke-width="2.5"/>
<path d="M16 11 L16 16 L19 19" stroke="#6BCF7F" stroke-width="2.5" stroke-linecap="round"/>
</svg>
</div>
<h3 class="oauth2-guide__security-title">Token TTL 활용</h3>
<p>expires_in 만큼 재사용하고,</p>
<p>만료 직전에 갱신을 트리거합니다.</p>
</article>
<div class="oauth2-2legged__panel">
<h3 class="oauth2-2legged__panel-title">응답 필드</h3>
<table class="oauth2-2legged__table">
<thead>
<tr>
<th>FIELD</th>
<th>TYPE</th>
<th>설명</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="oauth2-2legged__field-name">access_token</code></td>
<td>string</td>
<td>Bearer 호출에 사용할 토큰</td>
</tr>
<tr>
<td><code class="oauth2-2legged__field-name">token_type</code></td>
<td>string</td>
<td>고정값 "bearer"</td>
</tr>
<tr>
<td><code class="oauth2-2legged__field-name">expires_in</code></td>
<td>int</td>
<td>유효 시간(초). 기본 86400</td>
</tr>
<tr>
<td><code class="oauth2-2legged__field-name">scope</code></td>
<td>string</td>
<td>실제 부여된 권한 범위</td>
</tr>
<tr>
<td><code class="oauth2-2legged__field-name">jti</code></td>
<td>string</td>
<td>토큰 고유 ID (감사/취소용)</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- Section 8: CTA -->
<a class="oauth2-guide__cta" th:href="@{/service/oauth2-2legged-guide}">
<div class="oauth2-guide__cta-body">
<span class="oauth2-guide__cta-eyebrow">NEXT STEP</span>
<h2 class="oauth2-guide__cta-title">2-Legged 구현 가이드 보기</h2>
<p class="oauth2-guide__cta-desc">실제 토큰 발급·API 호출 방법을 코드 샘플과 함께 확인합니다.</p>
<span class="oauth2-guide__cta-button">개발 가이드 →</span>
<!-- Section 6: STEP 3 — API 호출 -->
<section class="oauth2-2legged__step" aria-labelledby="o2leg-step3-title">
<span class="oauth2-2legged__eyebrow">STEP 3</span>
<h2 class="oauth2-2legged__h2" id="o2leg-step3-title">발급 토큰으로 API 호출</h2>
<p class="oauth2-2legged__desc">Authorization 헤더에 Bearer 토큰을 실어 보호 자원 API 를 호출합니다.</p>
<div class="oauth2-2legged__step-grid">
<div class="oauth2-2legged__panel">
<h3 class="oauth2-2legged__panel-title">필수 헤더</h3>
<div class="oauth2-2legged__header-box">
<code class="oauth2-2legged__header-key">Authorization:</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 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> \
<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">'Accept: application/json'</span>
<span class="o2leg-g"># 응답</span>
HTTP/1.1 <span class="o2leg-g">200 OK</span>
{ <span class="o2leg-c">"accounts"</span>: [ ... ] }</pre>
</div>
</div>
<span class="oauth2-guide__cta-deco oauth2-guide__cta-deco--lg" aria-hidden="true"></span>
<span class="oauth2-guide__cta-deco oauth2-guide__cta-deco--sm" aria-hidden="true"></span>
</section>
<!-- Section 7: 대표 에러 응답 (표) -->
<section class="oauth2-2legged__errors" aria-labelledby="o2leg-errors-title">
<span class="oauth2-2legged__eyebrow">ERROR CODES</span>
<h2 class="oauth2-2legged__h2" id="o2leg-errors-title">대표 에러 응답</h2>
<div class="oauth2-2legged__panel">
<table class="oauth2-2legged__table oauth2-2legged__table--errors">
<thead>
<tr>
<th>HTTP</th>
<th>ERROR CODE</th>
<th>의미</th>
<th>해결 가이드</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="oauth2-2legged__status-badge oauth2-2legged__status-badge--400">400</span></td>
<td><code>invalid_request</code></td>
<td>필수 파라미터 누락/형식 오류</td>
<td>grant_type, client_id, client_secret 확인</td>
</tr>
<tr>
<td><span class="oauth2-2legged__status-badge oauth2-2legged__status-badge--400">401</span></td>
<td><code>invalid_client</code></td>
<td>자격증명 불일치 / 미승인</td>
<td>ClientID·Secret 재확인, 앱 상태 점검</td>
</tr>
<tr>
<td><span class="oauth2-2legged__status-badge oauth2-2legged__status-badge--400">401</span></td>
<td><code>invalid_token</code></td>
<td>만료/위·변조된 토큰</td>
<td>토큰 재발급 후 재시도</td>
</tr>
<tr>
<td><span class="oauth2-2legged__status-badge oauth2-2legged__status-badge--403">403</span></td>
<td><code>insufficient_scope</code></td>
<td>scope 권한 부족</td>
<td>필요 scope 추가 신청 후 재발급</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- Section 8: 에러 응답 예시 (단일 사례) -->
<section class="oauth2-2legged__error-example" aria-labelledby="o2leg-err-ex-title">
<span class="oauth2-2legged__eyebrow">ERROR RESPONSE</span>
<h2 class="oauth2-2legged__h2" id="o2leg-err-ex-title">에러 응답 예시</h2>
<p class="oauth2-2legged__desc">자격증명 불일치 시 401 Unauthorized 와 함께 다음 JSON 을 반환합니다.</p>
<div class="oauth2-2legged__step-grid">
<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>
<pre class="oauth2-2legged__code-block">{
<span class="o2leg-c">"error"</span>: <span class="o2leg-y">"invalid_client"</span>,
<span class="o2leg-c">"error_description"</span>:
<span class="o2leg-y">"Client authentication failed"</span>,
<span class="o2leg-c">"status"</span>: <span class="o2leg-p">401</span>,
<span class="o2leg-c">"timestamp"</span>: <span class="o2leg-y">"2026-06-10T11:23:45Z"</span>
}
<span class="o2leg-g"># 처리: ClientID·Secret 확인 후 재시도</span></pre>
</div>
<div class="oauth2-2legged__panel">
<h3 class="oauth2-2legged__panel-title">응답 필드</h3>
<table class="oauth2-2legged__table">
<thead>
<tr>
<th>FIELD</th>
<th>TYPE</th>
<th>설명</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="oauth2-2legged__field-name">error</code></td>
<td>string</td>
<td>에러 코드 (예: invalid_client)</td>
</tr>
<tr>
<td><code class="oauth2-2legged__field-name">error_description</code></td>
<td>string</td>
<td>사람이 읽을 수 있는 설명</td>
</tr>
<tr>
<td><code class="oauth2-2legged__field-name">status</code></td>
<td>int</td>
<td>HTTP 상태 코드</td>
</tr>
<tr>
<td><code class="oauth2-2legged__field-name">timestamp</code></td>
<td>string</td>
<td>에러 발생 시각 (ISO 8601)</td>
</tr>
</tbody>
</table>
<p class="oauth2-2legged__error-note">⚠ error 코드는 RFC 6749 표준 코드 또는 DJBank 확장 코드</p>
</div>
</div>
</section>
<!-- Section 9: API 목록 CTA -->
<a class="oauth2-2legged__cta" th:href="@{/apis}">
<div class="oauth2-2legged__cta-body">
<span class="oauth2-2legged__cta-eyebrow">EXPLORE</span>
<h2 class="oauth2-2legged__cta-title">API 목록 보러가기</h2>
<p class="oauth2-2legged__cta-desc">사용 가능한 DJBank Open API 카탈로그를 확인하세요.</p>
<span class="oauth2-2legged__cta-button">API 목록 →</span>
</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--sm" aria-hidden="true"></span>
</a>
</section>
@@ -36,8 +36,7 @@
<ul class="sub-menu">
<li><a th:href="@{/service/intro}">API 포탈 소개</a></li>
<li><a th:href="@{/service/guide}">회원가입 안내</a></li>
<li><a th:href="@{/service/oauth2-guide}">OAuth2 인증가이드</a></li>
<li><a th:href="@{/service/oauth2-2legged-guide}">OAuth2 2-Legged 가이드</a></li>
<li><a th:href="@{/service/oauth2-guide}">OAuth2 개발가이드</a></li>
</ul>
</li>
<li><a href="/apis" class="nav-link">오픈 API</a></li>