feature/admin #6
|
|
@ -54,6 +54,16 @@ dependencies {
|
|||
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
|
||||
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'
|
||||
|
||||
// TEST
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||
testImplementation 'org.testcontainers:testcontainers'
|
||||
testImplementation 'org.testcontainers:junit-jupiter'
|
||||
// 테스트 컨테이너 DB 의존성 참고
|
||||
// MySQL : https://www.testcontainers.org/modules/databases/mysql/
|
||||
// MariaDB : https://www.testcontainers.org/modules/databases/mariadb/
|
||||
// MongoDB : https://www.testcontainers.org/modules/databases/mongodb/
|
||||
// PostgreSQL: https://java.testcontainers.org/modules/databases/postgres/
|
||||
testImplementation 'org.testcontainers:mariadb'
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
|
||||
@font-face {
|
||||
font-family: 'Pretendard';
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: local('Pretendard Black'), url(../font/Pretendard-Black.woff2) format('woff2'), url(../font/Pretendard-Black.woff) format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Pretendard';
|
||||
font-weight: 800;
|
||||
font-display: swap;
|
||||
src: local('Pretendard ExtraBold'), url(../font/Pretendard-ExtraBold.woff2) format('woff2'), url(../font/Pretendard-ExtraBold.woff) format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Pretendard';
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: local('Pretendard Bold'), url(../font/Pretendard-Bold.woff2) format('woff2'), url(../font/Pretendard-Bold.woff) format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Pretendard';
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: local('Pretendard SemiBold'), url(../font/Pretendard-SemiBold.woff2) format('woff2'), url(../font/Pretendard-SemiBold.woff) format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Pretendard';
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: local('Pretendard Medium'), url(../font/Pretendard-Medium.woff2) format('woff2'), url(../font/Pretendard-Medium.woff) format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Pretendard';
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: local('Pretendard Regular'), url(../font/Pretendard-Regular.woff2) format('woff2'), url(../font/Pretendard-Regular.woff) format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Pretendard';
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: local('Pretendard Light'), url(../font/Pretendard-Light.woff2) format('woff2'), url(../font/Pretendard-Light.woff) format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Pretendard';
|
||||
font-weight: 200;
|
||||
font-display: swap;
|
||||
src: local('Pretendard ExtraLight'), url(../font/Pretendard-ExtraLight.woff2) format('woff2'), url(../font/Pretendard-ExtraLight.woff) format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Pretendard';
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: local('Pretendard Thin'), url(../font/Pretendard-Thin.woff2) format('woff2'), url(../font/Pretendard-Thin.woff) format('woff');
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
|
||||
/* style reset */
|
||||
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre, a,
|
||||
abbr, acsronym, address, big, cite, code, del,
|
||||
dfn, em, img, ins, kbd, q, s, samp, small,
|
||||
strike, strong, sub, sup, tt, var, b, u, i,
|
||||
center, dl, dt, dd, ol, ul, li, fieldset, form,
|
||||
label, legend, table, caption, tbody, tfoot,
|
||||
thead, tr, th, td, article, aside, canvas,
|
||||
details, embed, figure, figcaption, footer,
|
||||
header, hgroup, menu, nav, output, ruby, section,
|
||||
summary, time, mark, audio, video, button, a, input {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
box-sizing: border-box;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
-webkit-tap-highlight-color : rgba(0,0,0,0);
|
||||
line-height: 150%;
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block
|
||||
}
|
||||
body {
|
||||
line-height: 1
|
||||
}
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
blockquote, q {
|
||||
quotes: none
|
||||
}
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: '';
|
||||
content: none
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0
|
||||
}
|
||||
a {
|
||||
text-decoration: none
|
||||
}
|
||||
|
|
@ -0,0 +1,899 @@
|
|||
@charset "utf-8";
|
||||
@import 'reset.css';
|
||||
@import 'fonts.css';
|
||||
|
||||
:root {
|
||||
|
||||
/* color */
|
||||
--color-black: #000000;
|
||||
--color-white: #ffffff;
|
||||
--color-blue: #0059C5;
|
||||
--color-333: #333;
|
||||
--color-666: #666;
|
||||
--color-999: #999;
|
||||
--color-b6b6b6: #b6b6b6;
|
||||
--color-c6c6c6: #c6c6c6;
|
||||
--color-d6d6d6: #d6d6d6;
|
||||
--color-e6e6e6: #e6e6e6;
|
||||
--color-f5f5f5: #f5f5f5;
|
||||
--color-f9f9f9: #f9f9f9;
|
||||
--color-red: #ff0000;
|
||||
|
||||
/* font Size */
|
||||
--fs-48: 3rem;
|
||||
--fs-40: 2.5rem;
|
||||
--fs-36: 2.25rem;
|
||||
--fs-34: 2.125rem;
|
||||
--fs-30: 1.875rem;
|
||||
--fs-29: 1.813rem;
|
||||
--fs-28: 1.75rem;
|
||||
--fs-27: 1.688rem;
|
||||
--fs-26: 1.625rem;
|
||||
--fs-25: 1.563rem;
|
||||
--fs-24: 1.5rem;
|
||||
--fs-23: 1.438rem;
|
||||
--fs-22: 1.375rem;
|
||||
--fs-21: 1.3125rem;
|
||||
--fs-20: 1.25rem;
|
||||
--fs-19: 1.1875rem;
|
||||
--fs-18: 1.125rem;
|
||||
--fs-17: 1.0625rem;
|
||||
--fs-16: 1rem;
|
||||
--fs-15: 0.9375rem;
|
||||
--fs-14: 0.875rem;
|
||||
--fs-13: 0.8125rem;
|
||||
--fs-12: 0.75rem;
|
||||
--fs-11: 0.6875rem;
|
||||
|
||||
/* font Weight */
|
||||
--fw-300: 300;
|
||||
--fw-400: 400;
|
||||
--fw-500: 500;
|
||||
--fw-600: 600;
|
||||
--fw-700: 700;
|
||||
}
|
||||
|
||||
|
||||
/* 공통 */
|
||||
|
||||
.blind {
|
||||
visibility: hidden;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
font-size: 0;
|
||||
line-height: 0
|
||||
}
|
||||
|
||||
|
||||
/* link */
|
||||
|
||||
a:-webkit-any-link {
|
||||
color: -webkit-link;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:link, a:visited {
|
||||
color: var(--color-black);
|
||||
}
|
||||
a:hover {
|
||||
color: var(--color-blue);
|
||||
}
|
||||
a:active {
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
|
||||
/* button */
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
background-color: var(--color-white);
|
||||
border-radius: 3px;
|
||||
}
|
||||
button:active {
|
||||
opacity: .75;
|
||||
}
|
||||
button:disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
.btn_blue, .btn_white {
|
||||
width: 100%;
|
||||
font-size: var(--fs-14);
|
||||
font-weight: var(--fw-600);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.btn_blue {
|
||||
color: var(--color-white);
|
||||
background-color: var(--color-blue);
|
||||
border: 1px solid var(--color-blue);
|
||||
}
|
||||
.btn_white {
|
||||
color: var(--color-black);
|
||||
background-color: var(--color-white);
|
||||
border: 1px solid var(--color-c6c6c6);
|
||||
}
|
||||
.tb_button {
|
||||
position: relative;
|
||||
}
|
||||
.tb_button button {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 3px 10px;
|
||||
font-size: var(--fs-13);
|
||||
color: var(--color-333);
|
||||
border: 1px solid var(--color-c6c6c6);
|
||||
}
|
||||
.posi_right {
|
||||
max-width: 110px;
|
||||
float: right;
|
||||
position: relative;
|
||||
top: 35px;
|
||||
font-size: var(--fs-14);
|
||||
padding: 7px 10px;
|
||||
}
|
||||
.ico_save {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
|
||||
/* heading */
|
||||
|
||||
h2 {
|
||||
position: relative;
|
||||
font-size: var(--fs-18);
|
||||
font-weight: var(--fw-500);
|
||||
}
|
||||
|
||||
|
||||
/* input */
|
||||
|
||||
[tabindex='-1'], [tabindex='0'] {
|
||||
outline: none;
|
||||
}
|
||||
input {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance:none;
|
||||
appearance:none;
|
||||
height: 46px;
|
||||
border-bottom: 1px solid var(--color-d6d6d6);
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
font-size: var(--fs-16);
|
||||
transition: all .05s;
|
||||
}
|
||||
input:focus, input:active, input:hover {
|
||||
outline: none;
|
||||
}
|
||||
input[type=number] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
input[type=number]::-webkit-inner-spin-button,
|
||||
input[type=number]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
input::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
input:invalid {
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
:-moz-submit-invalid {
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
:-moz-ui-invalid {
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
input:focus-within {
|
||||
border-bottom: 2px solid var(--color-blue);
|
||||
}
|
||||
input:disabled {
|
||||
color: var(--color-999);
|
||||
background: var(--color-white);
|
||||
}
|
||||
input:read-only {
|
||||
color: var(--color-666);
|
||||
background: var(--color-white);
|
||||
}
|
||||
input:disabled:focus, input:disabled:active, input:disabled:hover {
|
||||
background: #fff !important;
|
||||
box-shadow: inset 0 -2px 0 var(--color-d6d6d6);
|
||||
}
|
||||
input::placeholder {
|
||||
color: var(--color-c6c6c6);
|
||||
}
|
||||
*::placeholder {
|
||||
font-weight: var(--fw-300);
|
||||
}
|
||||
input[type="password"] {
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
input.id[type="text"] {
|
||||
font-size: var(--fs-18);
|
||||
font-weight: var(--fw-700);
|
||||
}
|
||||
input.big[type="password"] {
|
||||
letter-spacing: -5px;
|
||||
font-size: var(--fs-30)
|
||||
}
|
||||
input.sm {
|
||||
width: 100%;
|
||||
box-shadow: inset 0 -1px 0 var(--color-d6d6d6);
|
||||
transition: all 0.1s ease;
|
||||
font-size: var(--fs-15);
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
}
|
||||
input.sm:focus, input.sm:active, input.sm:hover {
|
||||
box-shadow: inset 0 -2px 0 var(--color-blue);
|
||||
}
|
||||
input.sm:disabled:focus, input.sm:disabled:active, input.sm:disabled:hover {
|
||||
background: var(--color-white) !important;
|
||||
box-shadow: inset 0 -1px 0 var(--color-d6d6d6);
|
||||
}
|
||||
.inp {
|
||||
position: relative;
|
||||
}
|
||||
.err_msg {
|
||||
display: none;
|
||||
}
|
||||
.err .err_msg {
|
||||
display: block;
|
||||
color: var(--color-red);
|
||||
font-size: var(--fs-14);
|
||||
margin-top: 10px;
|
||||
}
|
||||
.btn_clear {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
transform: translateY(-50%);
|
||||
background: url('../images/ico_clear.svg') no-repeat center;
|
||||
background-size: 16px auto;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
|
||||
/* select */
|
||||
|
||||
select {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
padding: 6px 30px 6px 10px;
|
||||
border: 1px solid var(--color-c6c6c6);
|
||||
border-radius: 3px;
|
||||
background: url('../images/select_arrow.svg') no-repeat right 10px center;
|
||||
outline: none;
|
||||
transition: all .05s;
|
||||
}
|
||||
select:hover {
|
||||
border-color: var(--color-black);
|
||||
outline: none;
|
||||
}
|
||||
select:focus {
|
||||
border-color: var(--color-black);
|
||||
color: var(--color-black);
|
||||
outline: none;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
|
||||
/* flex */
|
||||
|
||||
.flexbox {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-moz-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
.flexitem {
|
||||
-webkit-box-flex: 1;
|
||||
-moz-box-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-moz-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-moz-box-orient: horizontal;
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
|
||||
/* checkbox */
|
||||
|
||||
.check_box {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
font-size: var(--fs-15);
|
||||
font-weight: var(--fw-400);
|
||||
}
|
||||
.check_box label {
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 0;
|
||||
cursor: pointer;
|
||||
transition: all .05s;
|
||||
}
|
||||
.check_box input[type="checkbox"] + label {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding-left: 30px;
|
||||
cursor: pointer;
|
||||
background: url('../images/chk_off.svg') no-repeat 0 0 / contain;
|
||||
background-size: 20px auto;
|
||||
}
|
||||
.check_box input[type='checkbox']:checked + label {
|
||||
background: url('../images/chk_on.svg') no-repeat 0 0 / contain;
|
||||
background-size: 20px auto;
|
||||
}
|
||||
.check_box input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/* radiobox */
|
||||
|
||||
.radio_box {
|
||||
position: relative;
|
||||
padding-left: 33px;
|
||||
padding-right: 30px;
|
||||
font-size: var(--fs-16);
|
||||
}
|
||||
.radio_box label {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
input[type="radio"] + label {
|
||||
position: absolute;
|
||||
display: block;
|
||||
height: 24px;
|
||||
padding-left: 26px;
|
||||
background: url('../images/radio_off.svg') no-repeat 0 0;
|
||||
background-size: 18px auto;
|
||||
background-position: 0 50%;
|
||||
}
|
||||
input[type='radio']:checked + label {
|
||||
background: url('../images/radio_on.svg') no-repeat 0 0;
|
||||
background-size: 18px auto;
|
||||
background-position: 0 50%;
|
||||
}
|
||||
input[type="radio"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/* list */
|
||||
|
||||
ul {
|
||||
overflow: hidden;
|
||||
}
|
||||
ul li {
|
||||
position: relative;
|
||||
}
|
||||
ul li em {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
|
||||
/* layout */
|
||||
|
||||
* {
|
||||
word-break: keep-all;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html, body {
|
||||
height: 100%;
|
||||
min-height: -webkit-fill-available;
|
||||
min-height: var(--vh-100);
|
||||
font-size: 16px;
|
||||
color: #1c1c1c;
|
||||
line-height: 150%;
|
||||
font-family: 'Pretendard', 'sans-serif';
|
||||
background-color: var(--color-white);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
/* scrollbar-gutter: stable; */
|
||||
}
|
||||
input, button, select {
|
||||
font-family: inherit;
|
||||
}
|
||||
header, main, footer {
|
||||
min-width: 768px;
|
||||
}
|
||||
main {
|
||||
width: 100%;
|
||||
}
|
||||
.login main {
|
||||
width: 100%;
|
||||
min-height: calc(100% - 103px);
|
||||
}
|
||||
|
||||
|
||||
/* header */
|
||||
|
||||
header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
background-color: var(--color-white);
|
||||
border-bottom: 1px solid var(--color-e6e6e6);
|
||||
box-shadow: 0 2px 12px rgb(0, 0, 0, 0.03);
|
||||
z-index: 1000;
|
||||
}
|
||||
header > div {
|
||||
position: relative;
|
||||
}
|
||||
header > div h1 {
|
||||
float: left;
|
||||
font-size: var(--fs-20);
|
||||
line-height: 50px;
|
||||
padding-left: 10px;
|
||||
font-weight: var(--fw-700);
|
||||
}
|
||||
header > div h1 span {
|
||||
position: relative;
|
||||
top: -2px;
|
||||
left: 10px;
|
||||
color: var(--color-666);
|
||||
font-weight: var(--fw-400);
|
||||
font-size: var(--fs-14);
|
||||
}
|
||||
header > div .language {
|
||||
position: relative;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
float: right;
|
||||
font-size: var(--fs-14);
|
||||
color: var(--color-333);
|
||||
}
|
||||
header > div .language span {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
header > div::after {
|
||||
content: " ";
|
||||
clear: both;
|
||||
}
|
||||
header > div .user_info {
|
||||
position: relative;
|
||||
top: 12px;
|
||||
right: 10px;
|
||||
float: right;
|
||||
}
|
||||
header > div .user_info a {
|
||||
font-size: var(--fs-14);
|
||||
color: var(--color-333);
|
||||
}
|
||||
header > div .user_info a:last-child {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* sidebar*/
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: calc(100vh - 61px)
|
||||
}
|
||||
.sidebar {
|
||||
width: 200px;
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
transition: width 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
.toggle-btn {
|
||||
position: absolute;
|
||||
top: 69px;
|
||||
left: 10px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 10px;
|
||||
background: url('../images/ico_max.svg') no-repeat center;
|
||||
color: #fff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.toggle-btn.active {
|
||||
background: url('../images/ico_mini.svg') no-repeat center;
|
||||
left: 5px;
|
||||
}
|
||||
.accordion {
|
||||
list-style-type: none;
|
||||
}
|
||||
.accordion-content {
|
||||
padding: 10px;
|
||||
display: none;
|
||||
}
|
||||
.sidebar .accordion {
|
||||
max-width: 300px;
|
||||
color: var(--color-white);
|
||||
margin-top: 50px;
|
||||
height: calc(100% - 200px);
|
||||
}
|
||||
.sidebar .accordion-header {
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
font-size: var(--fs-15);
|
||||
color: var(--color-e6e6e6);
|
||||
}
|
||||
.sidebar .accordion-header::after {
|
||||
content: '┼';
|
||||
float: right;
|
||||
font-size: 7px;
|
||||
}
|
||||
.sidebar .accordion-item .accordion-header.active::after {
|
||||
content: '━';
|
||||
font-size: 7px;
|
||||
}
|
||||
.accordion-item .accordion-header.active + .accordion-content {
|
||||
display: block;
|
||||
}
|
||||
.sidebar .sub_menu a {
|
||||
display: block;
|
||||
font-size: var(--fs-14);
|
||||
text-decoration: none;
|
||||
color: var(--color-e6e6e6);
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
|
||||
/* bread crum */
|
||||
|
||||
.con_header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
font-size: var(--fs-14);
|
||||
color: #999;
|
||||
padding-left: 48px;
|
||||
}
|
||||
.con_header.active {
|
||||
padding-left: 30px;
|
||||
}
|
||||
.con_header::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 11px;
|
||||
display: inline-block;
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background-color: var(--color-c6c6c6);
|
||||
}
|
||||
.con_header.active::before {
|
||||
content: none;
|
||||
}
|
||||
.con_header.active .breadcrum {
|
||||
padding-left: 0;
|
||||
}
|
||||
.con_header .breadcrum {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.con_header .breadcrum a {
|
||||
display: inline-block;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
text-decoration: none;
|
||||
color: var(--color-999);
|
||||
}
|
||||
.con_header .breadcrum a::after {
|
||||
content: ">";
|
||||
display: inline-block;
|
||||
margin: 0 4px 0 6px;
|
||||
font-size: var(--fs-12);
|
||||
color: #c6c6c6;
|
||||
}
|
||||
.con_header .breadcrum a:last-child:after {
|
||||
content: none;
|
||||
}
|
||||
.con_header .breadcrum a:last-child {
|
||||
color: var(--color-333);
|
||||
}
|
||||
|
||||
|
||||
/* footer */
|
||||
|
||||
.login footer {
|
||||
position: relative;
|
||||
}
|
||||
.login footer > div {
|
||||
padding: 20px;
|
||||
}
|
||||
.login .foot_head {
|
||||
overflow: hidden;
|
||||
height: 30px;
|
||||
}
|
||||
.login .foot_head h2 {
|
||||
position: relative;
|
||||
top: -17px;
|
||||
float: right;
|
||||
margin-left: 20px;
|
||||
}
|
||||
.login .foot_head h2 img {
|
||||
height: 11px;
|
||||
}
|
||||
.login .foot_head a {
|
||||
float: right;
|
||||
font-size: var(--fs-13);
|
||||
color: var(--color-333);
|
||||
}
|
||||
.login .foot_copy {
|
||||
font-size: var(--fs-11);
|
||||
color: var(--color-666);
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
footer {
|
||||
min-width: 200px;
|
||||
}
|
||||
.foot_head h2 {
|
||||
padding-left: 10px;
|
||||
}
|
||||
.foot_head h2 img {
|
||||
width: 60px;
|
||||
}
|
||||
.foot_head a {
|
||||
color: var(--color-c6c6c6);
|
||||
font-size: var(--fs-12);
|
||||
padding-left: 10px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.foot_copy {
|
||||
font-size: var(--fs-11);
|
||||
word-break: break-all;
|
||||
width: 200px;
|
||||
padding: 0 10px 10px 10px;
|
||||
color: var(--color-999);
|
||||
}
|
||||
|
||||
/* Heading */
|
||||
|
||||
h2 {
|
||||
font-size: var(--fs-30);
|
||||
font-weight: var(--fw-700);
|
||||
}
|
||||
h3 {
|
||||
font-size: var(--fs-20);
|
||||
font-weight: var(--fw-700);
|
||||
}
|
||||
|
||||
|
||||
/* scroll */
|
||||
|
||||
.scrollable::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
.scrollable::-webkit-scrollbar-track {
|
||||
background-color: #fff;
|
||||
}
|
||||
.scrollable::-webkit-scrollbar-thumb {
|
||||
background-color: #c6c6c6;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.scrollable::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #999;
|
||||
}
|
||||
|
||||
|
||||
/* pagination */
|
||||
|
||||
.pagination {
|
||||
display: inline-block;
|
||||
margin-top: 30px;
|
||||
}
|
||||
.pagination a {
|
||||
color: black;
|
||||
float: left;
|
||||
font-size: var(--fs-13);
|
||||
padding: 8px 14px;
|
||||
text-decoration: none;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--color-c6c6c6);
|
||||
margin: 0 5px;
|
||||
}
|
||||
.pagination a.active {
|
||||
background-color: var(--color-blue);
|
||||
color: var(--color-white);
|
||||
border-radius: 5px;
|
||||
}
|
||||
.pagination a:hover:not(.active) {
|
||||
background-color: #ddd;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
|
||||
/* table */
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table th {
|
||||
background-color: var(--color-f5f5f5);
|
||||
font-size: var(--fs-14);
|
||||
font-weight: var(--fw-600);
|
||||
padding: 8px 10px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
table td {
|
||||
border-top: 1px solid var(--color-d6d6d6);
|
||||
font-size: var(--fs-14);
|
||||
color: var(--color-333);
|
||||
padding: 10px;
|
||||
}
|
||||
.tb_one {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.tb_two {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
.tb_two .flex1 {
|
||||
flex: 1;
|
||||
}
|
||||
.tb_one.sty_box {
|
||||
position: relative;
|
||||
border: 1px solid var(--color-d6d6d6);
|
||||
}
|
||||
.tb_sty01 th, .tb_sty01 td {
|
||||
text-align: center;
|
||||
}
|
||||
table.num_txt td:first-child {
|
||||
font-size: var(--fs-13);
|
||||
}
|
||||
.scrollable-container {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
/* 로그인 */
|
||||
|
||||
body.login {
|
||||
background-color: var(--color-white);
|
||||
}
|
||||
body.login select {
|
||||
background-size: 8px auto;
|
||||
}
|
||||
.login_wrapper {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
padding-top: 10vh;
|
||||
}
|
||||
.login_wrapper h2 {
|
||||
font-size: var(--fs-36);
|
||||
font-weight: var(--fw-700);
|
||||
}
|
||||
.login_wrapper .check_form {
|
||||
text-align: right;
|
||||
margin-top: 30px;
|
||||
}
|
||||
.login_wrapper .login_box .check_box {
|
||||
font-size: var(--fs-14);
|
||||
color: var(--color-333);
|
||||
padding-left: 25px;
|
||||
padding-right: 0;
|
||||
}
|
||||
.login_wrapper .login_box .check_box input[type="checkbox"] + label {
|
||||
background-size: 16px auto;
|
||||
background-position: left 0 top 40%;
|
||||
}
|
||||
.login_wrapper .login_box .input_form {
|
||||
padding: 10px 0;
|
||||
}
|
||||
.login_wrapper .login_box .input_form p {
|
||||
padding: 10px 0;
|
||||
}
|
||||
.login_wrapper .login_box .input_form input {
|
||||
width: 100%;
|
||||
font-size: var(--fs-18);
|
||||
font-weight: var(--fw-600);
|
||||
}
|
||||
.login_wrapper .login_box .input_form > button {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.login_wrapper .out_link {
|
||||
text-align: right;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.login_wrapper .out_link a {
|
||||
font-size: var(--fs-15);
|
||||
color: var(--color-333);
|
||||
}
|
||||
.login_wrapper .out_link a:last-child {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.login_wrapper .sub_discription {
|
||||
font-size: var(--fs-15);
|
||||
color: var(--color-333);
|
||||
margin-top: 60px;
|
||||
}
|
||||
.login_wrapper .sub_discription li {
|
||||
position: relative;
|
||||
padding: 2px 10px;
|
||||
}
|
||||
|
||||
|
||||
/* content */
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
padding: 70px 30px 50px 30px;
|
||||
}
|
||||
.con_wrap h3 {
|
||||
display: inline-block;
|
||||
margin-top: 40px;
|
||||
}
|
||||
.middle_box {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
.middle_box button {
|
||||
position: absolute;
|
||||
top: 60%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 10px 15px;
|
||||
max-width: 90px;
|
||||
}
|
||||
.discription {
|
||||
font-size: var(--fs-15);
|
||||
margin-top: 10px;
|
||||
color: var(--color-333);
|
||||
}
|
||||
|
|
@ -0,0 +1,353 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>SRECUI - Partner Inventory System</title>
|
||||
<link rel="stylesheet" href="../css/style.css">
|
||||
<script src="../js/motion.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div>
|
||||
<h1>
|
||||
<img src="../images/logo_secui.svg" alt="SRECUI logo">
|
||||
<span>- Partner Inventory System</span>
|
||||
</h1>
|
||||
<div class="user_info">
|
||||
<a href="#none">madzoneviper</a>
|
||||
<a href="#none">로그아웃</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<div class="container">
|
||||
<div class="sidebar" id="sidebar" style="width: 200px;">
|
||||
<ul class="accordion">
|
||||
<li class="accordion-item">
|
||||
<div class="accordion-header">계약관리</div>
|
||||
<div class="accordion-content">
|
||||
<ul class="sub_menu">
|
||||
<li>
|
||||
<a href="#none">유지관리계약</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#none">유지관리 변경</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#none">해약서 발급</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#none">하도급 계약</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#none">파트너 계약</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#none">상품구매/용역계약</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#none">해약서 발급</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="accordion-item">
|
||||
<div class="accordion-header">MY 정보</div>
|
||||
<div class="accordion-content">
|
||||
<ul class="sub_menu">
|
||||
<li>
|
||||
<a href="#none">Submenu1</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#none">Submenu2</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#none">Submenu3</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="accordion-item">
|
||||
<div class="accordion-header">시스템운영</div>
|
||||
<div class="accordion-content">
|
||||
<ul class="sub_menu">
|
||||
<li>
|
||||
<a href="#none">Submenu1</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#none">Submenu2</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#none">Submenu3</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="accordion-item">
|
||||
<div class="accordion-header">관리자</div>
|
||||
<div class="accordion-content">
|
||||
<ul class="sub_menu">
|
||||
<li>
|
||||
<a href="#none">Submenu1</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#none">Submenu2</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#none">Submenu3</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<footer>
|
||||
<div>
|
||||
<div class="foot_head">
|
||||
<h2><img src="../images/logo_secui_gray.svg" alt="secui logo"></h2>
|
||||
<a href="#none">개인정보취급방침</a>
|
||||
</div>
|
||||
<p class="foot_copy">
|
||||
(03161) 서울특별시 종로구 종로 51, 3,5,6층 (종로2가, 종로타워)<br>
|
||||
<span>COPYRIGHT(C) 2018 SECUI ALL RIGHTS RESERVED</span>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="con_header active">
|
||||
<div class="breadcrum">
|
||||
<a href="#none">Dashboard</a>
|
||||
<a href="#none">Section01</a>
|
||||
<a href="#none">Submenu02</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="con_wrap">
|
||||
<h2>권한관리</h2>
|
||||
<p class="discription">이 페이지에서는 인트라넷 사용자들의 권한을 관리하고 조정할 수 있습니다.</p>
|
||||
|
||||
<h3>그룹목록</h3>
|
||||
<button type="button" class="btn_blue posi_right">신규그룹 등록</button>
|
||||
<div class="tb_one">
|
||||
<table class="tb_sty01 num_txt">
|
||||
<colgroup>
|
||||
<col style="width: 5%;">
|
||||
<col style="width: 20%;">
|
||||
<col style="width: 20%;">
|
||||
<col style="width: 20%;">
|
||||
<col style="width: 20%;">
|
||||
<col style="width: 5%;">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>그룹번호</th>
|
||||
<th>그룹명</th>
|
||||
<th>그룹권한</th>
|
||||
<th>그룹설명</th>
|
||||
<th>상태</th>
|
||||
<th>수정</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>시스템관리자</td>
|
||||
<td>시스템관리자</td>
|
||||
<td>시스템관리자</td>
|
||||
<td>사용</td>
|
||||
<td class="tb_button"><button type="button">수정</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>운영관리자</td>
|
||||
<td>관리자</td>
|
||||
<td>운영관리자</td>
|
||||
<td>사용</td>
|
||||
<td class="tb_button"><button type="button">수정</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>직원사용자</td>
|
||||
<td>직원사용자</td>
|
||||
<td>시큐아이직원 일반사용자</td>
|
||||
<td>사용</td>
|
||||
<td class="tb_button"><button type="button">수정</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td>외부사용자</td>
|
||||
<td>외부사용자</td>
|
||||
<td>외부사용자</td>
|
||||
<td>사용</td>
|
||||
<td class="tb_button"><button type="button">수정</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td>파트너</td>
|
||||
<td>외부사용자</td>
|
||||
<td>솔루션파트너</td>
|
||||
<td>사용</td>
|
||||
<td class="tb_button"><button type="button">수정</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="pagination">
|
||||
<a href="#">«</a>
|
||||
<a href="#">1</a>
|
||||
<a href="#" class="active">2</a>
|
||||
<a href="#">3</a>
|
||||
<a href="#">4</a>
|
||||
<a href="#">5</a>
|
||||
<a href="#">6</a>
|
||||
<a href="#">»</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tb_two">
|
||||
<div class="flex1">
|
||||
<h3>등록 메뉴</h3>
|
||||
<div class="tb_one sty_box scrollable-container scrollable">
|
||||
<table>
|
||||
<colgroup>
|
||||
<col style="width: 50%;">
|
||||
<col style="width: 50%;">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>메뉴ID</th>
|
||||
<th>메뉴명</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>WP_PTS_AD_CM</td>
|
||||
<td>거래처정보</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WP_PTS_AD_PM</td>
|
||||
<td>상품정보</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WP_PTS_AD_UM</td>
|
||||
<td>사용자관리</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WP_PTS_GE_AD_RES</td>
|
||||
<td>하도급 계약조회(관)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WP_PTS_GE_AGR</td>
|
||||
<td>하도급 계약체결</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WP_PTS_GE_APP</td>
|
||||
<td>하도급 계약견적 승인</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WP_PTS_GE_EST_LIST</td>
|
||||
<td>하도급 계약견적 작성</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WP_PTS_GE_MST</td>
|
||||
<td>하도급 계약견적 요청</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WP_PTS_GE_RES</td>
|
||||
<td>하도급 계약조회</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WP_PTS_IB_BUSINESS</td>
|
||||
<td>영업자료</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WP_PTS_IB_DEP</td>
|
||||
<td>담당부서 안내</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WP_PTS_IB_FAQ</td>
|
||||
<td>FAQ</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="middle_box">
|
||||
<button type="button" class="btn_blue ico_save">변경내용 저장</button>
|
||||
</div>
|
||||
<div class="flex1">
|
||||
<h3>미등록 메뉴</h3>
|
||||
<div class="tb_one sty_box scrollable-container scrollable">
|
||||
<table>
|
||||
<colgroup>
|
||||
<col style="width: 50%;">
|
||||
<col style="width: 50%;">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>메뉴ID</th>
|
||||
<th>메뉴명</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>WP_PTS_AD_CM</td>
|
||||
<td>거래처정보</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WP_PTS_AD_PM</td>
|
||||
<td>상품정보</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WP_PTS_AD_UM</td>
|
||||
<td>사용자관리</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WP_PTS_GE_AD_RES</td>
|
||||
<td>하도급 계약조회(관)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WP_PTS_GE_AGR</td>
|
||||
<td>하도급 계약체결</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WP_PTS_GE_APP</td>
|
||||
<td>하도급 계약견적 승인</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WP_PTS_GE_EST_LIST</td>
|
||||
<td>하도급 계약견적 작성</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WP_PTS_GE_MST</td>
|
||||
<td>하도급 계약견적 요청</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WP_PTS_GE_RES</td>
|
||||
<td>하도급 계약조회</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WP_PTS_IB_BUSINESS</td>
|
||||
<td>영업자료</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WP_PTS_IB_DEP</td>
|
||||
<td>담당부서 안내</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WP_PTS_IB_FAQ</td>
|
||||
<td>FAQ</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<button class="toggle-btn active" onclick="toggleSidebar()"><span class="blind">메뉴 열기/닫기</span></button>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>SRECUI - Partner Inventory System</title>
|
||||
<link rel="stylesheet" href="../css/style.css">
|
||||
<script src="../js/motion.js"></script>
|
||||
</head>
|
||||
<body class="login">
|
||||
<header>
|
||||
<div>
|
||||
<h1>
|
||||
<img src="../images/logo_secui.svg" alt="SRECUI logo">
|
||||
<span>- Partner Inventory System</span>
|
||||
</h1>
|
||||
<div class="language">
|
||||
language :
|
||||
<span>
|
||||
<select>
|
||||
<option>한국어</option>
|
||||
<option>English</option>
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<div class="login_wrapper">
|
||||
<h2>LOGIN</h2>
|
||||
<div class="login_box">
|
||||
<div class="check_form">
|
||||
<p class="check_box">
|
||||
<input type="checkbox" id="c2" name="c" class="checkbox">
|
||||
<label for="c2" class="gtr small"></label>아이디 저장
|
||||
</p>
|
||||
</div>
|
||||
<div class="input_form">
|
||||
<p>
|
||||
<label for="inp1" class="inp clear err"> <!-- 에러메세지 뿌릴때는 .err 클래스 추가 -->
|
||||
<input type="text" id="inp1" class="inp_states" placeholder="사용자 이메일 입력" autocomplete="off">
|
||||
<span class="focus-bg"></span>
|
||||
<span class="err_msg">에러메세지가 노출됩니다.</span>
|
||||
<button class="btn_clear"><span class="blind">삭제</span></button>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label for="inp2" class="inp clear">
|
||||
<input type="password" id="inp2" class="inp_states" placeholder="비밀번호 입력" autocomplete="off">
|
||||
<span class="focus-bg"></span>
|
||||
<span class="err_msg">에러메세지가 노출됩니다.</span>
|
||||
<button class="btn_clear"><span class="blind">삭제</span></button>
|
||||
</label>
|
||||
</p>
|
||||
<button type="button" class="btn_blue">로그인</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="out_link">
|
||||
<a href="#none">신규회원가입</a>
|
||||
<a href="#none">아이디/비밀번호 찾기</a>
|
||||
</div>
|
||||
<ul class="sub_discription">
|
||||
<li><em>·</em> SECUI는 Partner Inventory System입니다.</li>
|
||||
<li><em>·</em> 처음 방문하시는 사용자는 신규회원가입 후 사용해 주세요.</li>
|
||||
<li><em>·</em> 본 시스템은 Internet Explorer를 지원하지 않습니다.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
<div>
|
||||
<div class="foot_head">
|
||||
<h2><img src="../images/logo_secui_gray.svg" alt="secui logo"></h2>
|
||||
<a href="#none">개인정보취급방침</a>
|
||||
</div>
|
||||
<p class="foot_copy">
|
||||
(03161) 서울특별시 종로구 종로 51, 3,5,6층 (종로2가, 종로타워)<br>
|
||||
<span>COPYRIGHT(C) 2018 SECUI ALL RIGHTS RESERVED</span>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Scrollable Table with Fixed Header</title>
|
||||
<style>
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
th, td {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
.scrollable-container {
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="scrollable-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Header 1</th>
|
||||
<th>Header 2</th>
|
||||
<th>Header 3</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- 10줄 예시 데이터 -->
|
||||
<tr>
|
||||
<td>Data 1</td>
|
||||
<td>Data 2</td>
|
||||
<td>Data 3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Data 4</td>
|
||||
<td>Data 5</td>
|
||||
<td>Data 6</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Data 7</td>
|
||||
<td>Data 8</td>
|
||||
<td>Data 9</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Data 10</td>
|
||||
<td>Data 11</td>
|
||||
<td>Data 12</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Data 13</td>
|
||||
<td>Data 14</td>
|
||||
<td>Data 15</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Data 16</td>
|
||||
<td>Data 17</td>
|
||||
<td>Data 18</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Data 19</td>
|
||||
<td>Data 20</td>
|
||||
<td>Data 21</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Data 22</td>
|
||||
<td>Data 23</td>
|
||||
<td>Data 24</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Data 25</td>
|
||||
<td>Data 26</td>
|
||||
<td>Data 27</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Data 28</td>
|
||||
<td>Data 29</td>
|
||||
<td>Data 30</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
|
||||
<g id="chk_off" transform="translate(0 0.355)">
|
||||
<g id="사각형_142" data-name="사각형 142" transform="translate(0 -0.355)" fill="#fff" stroke="#b5bbc3" stroke-width="1">
|
||||
<rect width="18" height="18" rx="4" stroke="none"/>
|
||||
<rect x="0.5" y="0.5" width="17" height="17" rx="3.5" fill="none"/>
|
||||
</g>
|
||||
<g id="그룹_381" data-name="그룹 381">
|
||||
<g id="사각형_142-2" data-name="사각형 142" transform="translate(0 -0.355)" fill="#fff" stroke="#333" stroke-width="1">
|
||||
<rect width="18" height="18" rx="3" stroke="none"/>
|
||||
<rect x="0.5" y="0.5" width="17" height="17" rx="2.5" fill="none"/>
|
||||
</g>
|
||||
<g id="그룹_277" data-name="그룹 277" transform="translate(4.671 5.835)">
|
||||
<line id="선_31" data-name="선 31" x2="3.114" y2="3.114" transform="translate(0 2.076)" fill="none" stroke="#fff" stroke-linecap="round" stroke-width="1.8"/>
|
||||
<line id="선_32" data-name="선 32" y1="5.19" x2="5.19" transform="translate(3.114)" fill="none" stroke="#fff" stroke-linecap="round" stroke-width="1.8"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -0,0 +1,15 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
|
||||
<g id="chk_on" transform="translate(0 0.355)">
|
||||
<g id="사각형_142" data-name="사각형 142" transform="translate(0 -0.355)" fill="#fff" stroke="#b5bbc3" stroke-width="1">
|
||||
<rect width="18" height="18" rx="4" stroke="none"/>
|
||||
<rect x="0.5" y="0.5" width="17" height="17" rx="3.5" fill="none"/>
|
||||
</g>
|
||||
<g id="그룹_381" data-name="그룹 381">
|
||||
<rect id="사각형_142-2" data-name="사각형 142" width="18" height="18" rx="3" transform="translate(0 -0.355)" fill="#0059c5"/>
|
||||
<g id="그룹_277" data-name="그룹 277" transform="translate(4.671 5.835)">
|
||||
<line id="선_31" data-name="선 31" x2="3.114" y2="3.114" transform="translate(0 2.076)" fill="none" stroke="#fff" stroke-linecap="round" stroke-width="1.8"/>
|
||||
<line id="선_32" data-name="선 32" y1="5.19" x2="5.19" transform="translate(3.114)" fill="none" stroke="#fff" stroke-linecap="round" stroke-width="1.8"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
|
|
@ -0,0 +1,7 @@
|
|||
<svg id="ico_clear" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
|
||||
<circle id="타원_73" data-name="타원 73" cx="10" cy="10" r="10" fill="#999"/>
|
||||
<g id="그룹_2783" data-name="그룹 2783" transform="translate(6.5 6.5)">
|
||||
<line id="선_94" data-name="선 94" x2="7" y2="7" fill="none" stroke="#fff" stroke-width="2"/>
|
||||
<line id="선_95" data-name="선 95" x1="7" y2="7" fill="none" stroke="#fff" stroke-width="2"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 475 B |
|
|
@ -0,0 +1,16 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="17" height="15" viewBox="0 0 17 15">
|
||||
<g id="ico_max" transform="translate(-5 -8)">
|
||||
<g id="그룹_337" data-name="그룹 337" transform="translate(5 8)">
|
||||
<g id="그룹_405" data-name="그룹 405">
|
||||
<g id="사각형_210" data-name="사각형 210" fill="rgba(255,255,255,0)" stroke="#000" stroke-width="1">
|
||||
<rect width="6" height="15" stroke="none"/>
|
||||
<rect x="0.5" y="0.5" width="5" height="14" fill="none"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="그룹_404" data-name="그룹 404" transform="translate(8.697 4)">
|
||||
<path id="패스_1216" data-name="패스 1216" d="M354.9,85.187l3.42,3.5-3.42,3.5" transform="translate(-350.714 -85.187)" fill="none" stroke="#000" stroke-width="1"/>
|
||||
<path id="패스_1217" data-name="패스 1217" d="M362.362,87.11h-7.285" transform="translate(-355.078 -83.61)" fill="none" stroke="#000" stroke-width="1"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 971 B |
|
|
@ -0,0 +1,16 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16.301" height="15" viewBox="0 0 16.301 15">
|
||||
<g id="ico_mini" transform="translate(-5 -8)">
|
||||
<g id="그룹_337" data-name="그룹 337" transform="translate(5 8)">
|
||||
<g id="그룹_405" data-name="그룹 405">
|
||||
<g id="사각형_210" data-name="사각형 210" fill="rgba(255,255,255,0)" stroke="#fff" stroke-width="1">
|
||||
<rect width="6" height="15" stroke="none"/>
|
||||
<rect x="0.5" y="0.5" width="5" height="14" fill="none"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="그룹_404" data-name="그룹 404" transform="translate(8.697 4)">
|
||||
<path id="패스_1216" data-name="패스 1216" d="M358.318,85.187l-3.42,3.5,3.42,3.5" transform="translate(-354.898 -85.187)" fill="none" stroke="#fff" stroke-width="1"/>
|
||||
<path id="패스_1217" data-name="패스 1217" d="M355.078,87.11h7.285" transform="translate(-354.758 -83.61)" fill="none" stroke="#fff" stroke-width="1"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 982 B |
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="91.263" height="16" viewBox="0 0 91.263 16">
|
||||
<g id="logo_secui" transform="translate(-544 -198.999)">
|
||||
<path id="합치기_1" data-name="합치기 1" d="M87.31,16V0h3.953V16ZM5.336,15.872H0c.8-3.683,3.57-3.478,3.57-3.478h10.1a.161.161,0,0,0,.163-.157v-.489a.174.174,0,0,0-.12-.146L2.282,7.417A2.554,2.554,0,0,1,.348,4.575V2.264C.348.9,1.109.025,2.8.025H17.6s-.456,3.285-3.617,3.382H4.053a.155.155,0,0,0-.162.163v.459A.172.172,0,0,0,4,4.175L15.737,8.427a2.435,2.435,0,0,1,1.739,2.542v2.7c0,1.033-.547,2.2-2.779,2.2H5.336Zm38.917,0c-1,0-3.012-.205-3.012-2.7V2.723c0-2.51,1.941-2.7,3.012-2.7H58.679s-.567,3.367-3.572,3.334L53.345,3.4l-7.975.009a.16.16,0,0,0-.158.153v8.749a.163.163,0,0,0,.158.166H58.148v3.392Zm-20.486,0c-1.155,0-2.976-.205-2.976-2.7V2.723c0-2.512,1.931-2.7,2.976-2.7H38.284a3.779,3.779,0,0,1-3.76,3.34L32.961,3.4l-8.09.011a.157.157,0,0,0-.156.153V6.15s.027.134.151.134H37.995a3.787,3.787,0,0,1-3.57,3.28l-1.807,0H24.849a.165.165,0,0,0-.14.171v2.574a.163.163,0,0,0,.162.166H37.178v3.392Zm41.063,0c-1.764,0-2.978-.444-2.978-2.686V.007h3.913V12.413a.17.17,0,0,0,.154.171h9.365a.165.165,0,0,0,.162-.171V.007h3.911V13.181c0,2.242-1.286,2.686-2.966,2.686Z" transform="translate(544 198.999)" fill="#0059c5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="91.263" height="16" viewBox="0 0 91.263 16">
|
||||
<g id="logo_secui_gray" transform="translate(-544 -198.999)">
|
||||
<path id="합치기_1" data-name="합치기 1" d="M87.31,16V0h3.953V16ZM5.336,15.872H0c.8-3.683,3.57-3.478,3.57-3.478h10.1a.161.161,0,0,0,.163-.157v-.489a.174.174,0,0,0-.12-.146L2.282,7.417A2.554,2.554,0,0,1,.348,4.575V2.264C.348.9,1.109.025,2.8.025H17.6s-.456,3.285-3.617,3.382H4.053a.155.155,0,0,0-.162.163v.459A.172.172,0,0,0,4,4.175L15.737,8.427a2.435,2.435,0,0,1,1.739,2.542v2.7c0,1.033-.547,2.2-2.779,2.2H5.336Zm38.917,0c-1,0-3.012-.205-3.012-2.7V2.723c0-2.51,1.941-2.7,3.012-2.7H58.679s-.567,3.367-3.572,3.334L53.345,3.4l-7.975.009a.16.16,0,0,0-.158.153v8.749a.163.163,0,0,0,.158.166H58.148v3.392Zm-20.486,0c-1.155,0-2.976-.205-2.976-2.7V2.723c0-2.512,1.931-2.7,2.976-2.7H38.284a3.779,3.779,0,0,1-3.76,3.34L32.961,3.4l-8.09.011a.157.157,0,0,0-.156.153V6.15s.027.134.151.134H37.995a3.787,3.787,0,0,1-3.57,3.28l-1.807,0H24.849a.165.165,0,0,0-.14.171v2.574a.163.163,0,0,0,.162.166H37.178v3.392Zm41.063,0c-1.764,0-2.978-.444-2.978-2.686V.007h3.913V12.413a.17.17,0,0,0,.154.171h9.365a.165.165,0,0,0,.162-.171V.007h3.911V13.181c0,2.242-1.286,2.686-2.966,2.686Z" transform="translate(544 198.999)" fill="#999"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="11.061" height="6.591" viewBox="0 0 11.061 6.591">
|
||||
<path id="select_arrow" d="M402.877,21.831l5,5,5-5" transform="translate(-402.347 -21.301)" fill="none" stroke="#090909" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 249 B |
|
|
@ -0,0 +1,88 @@
|
|||
|
||||
|
||||
// header 높이 만큼 body의 padding-top으로 설정
|
||||
|
||||
window.addEventListener('load', function() {
|
||||
var headerHeight = document.querySelector('header').offsetHeight;
|
||||
document.body.style.paddingTop = headerHeight + 'px';
|
||||
});
|
||||
|
||||
|
||||
// Sidebar 열고 닫기
|
||||
|
||||
function toggleSidebar() {
|
||||
var sidebar = document.querySelector('.sidebar');
|
||||
var contentHeader = document.querySelector('.content .con_header');
|
||||
var toggleBtn = document.querySelector('.toggle-btn');
|
||||
|
||||
if (sidebar.style.width === '200px') {
|
||||
sidebar.style.width = '0';
|
||||
toggleBtn.classList.remove('active');
|
||||
contentHeader.classList.remove('active');
|
||||
} else {
|
||||
sidebar.style.width = '200px';
|
||||
toggleBtn.classList.add('active');
|
||||
contentHeader.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
|
||||
// Sidebar 아코디언 메뉴
|
||||
const accordionItems = document.querySelectorAll('.accordion-header');
|
||||
|
||||
accordionItems.forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
item.classList.toggle('active');
|
||||
});
|
||||
});
|
||||
|
||||
// INPUT 내용 삭제
|
||||
document.querySelectorAll(".btn_clear").forEach(function(btn) {
|
||||
btn.style.display = "none";
|
||||
});
|
||||
|
||||
document.querySelectorAll(".inp input").forEach(function(input) {
|
||||
input.addEventListener("input", function() {
|
||||
var parentLabel = this.closest(".inp");
|
||||
var clearButton = parentLabel.querySelector(".btn_clear");
|
||||
|
||||
if (this.value !== "") {
|
||||
clearButton.style.display = "block";
|
||||
} else {
|
||||
clearButton.style.display = "none";
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll(".btn_clear").forEach(function(btn) {
|
||||
btn.addEventListener("click", function() {
|
||||
var parentLabel = this.closest(".inp");
|
||||
var input = parentLabel.querySelector("input");
|
||||
|
||||
input.value = "";
|
||||
this.style.display = "none";
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// 입력값 변경시 호출 함수 등록
|
||||
document.querySelectorAll(".inp_states").forEach(function(inputElement) {
|
||||
inputElement.addEventListener("input", function() {
|
||||
removeErrorClass(this);
|
||||
});
|
||||
});
|
||||
|
||||
// 에러 클래스 제거 함수
|
||||
function removeErrorClass(inputElement) {
|
||||
if (inputElement.value.trim() !== "") {
|
||||
inputElement.closest(".inp").classList.remove("err");
|
||||
inputElement.nextElementSibling.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
package com.bpgroup.poc.admin.app.login;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Getter
|
||||
public class LoginResult {
|
||||
private Long id;
|
||||
private String loginId;
|
||||
private String name;
|
||||
private String email;
|
||||
|
||||
private Set<MenuInfo> menus = new HashSet<>();
|
||||
|
||||
public static LoginResult of(Long id, String loginId, String name, String email, Set<MenuInfo> menus) {
|
||||
LoginResult loginResult = new LoginResult();
|
||||
loginResult.id = id;
|
||||
loginResult.loginId = loginId;
|
||||
loginResult.name = name;
|
||||
loginResult.email = email;
|
||||
loginResult.menus = menus;
|
||||
return loginResult;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
public static class MenuInfo {
|
||||
private String menuGroupUri;
|
||||
private String menuGroupName;
|
||||
private Integer menuGroupSortOrder;
|
||||
private String menuUri;
|
||||
private String menuName;
|
||||
private Integer menuSortOrder;
|
||||
|
||||
public static MenuInfo of(
|
||||
String menuGroupUri,
|
||||
String menuGroupName,
|
||||
Integer menuGroupSortOrder,
|
||||
String menuUri,
|
||||
String menuName,
|
||||
Integer menuSortOrder
|
||||
) {
|
||||
MenuInfo menuInfo = new MenuInfo();
|
||||
menuInfo.menuGroupUri = menuGroupUri;
|
||||
menuInfo.menuGroupName = menuGroupName;
|
||||
menuInfo.menuGroupSortOrder = menuGroupSortOrder;
|
||||
menuInfo.menuUri = menuUri;
|
||||
menuInfo.menuName = menuName;
|
||||
menuInfo.menuSortOrder = menuSortOrder;
|
||||
return menuInfo;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
package com.bpgroup.poc.admin.app.login;
|
||||
|
||||
import com.bpgroup.poc.admin.app.login.exception.AdministratorNotFoundException;
|
||||
import com.bpgroup.poc.admin.app.login.exception.DoNotHaveAnyMenuException;
|
||||
import com.bpgroup.poc.admin.app.login.exception.InvalidPasswordException;
|
||||
import com.bpgroup.poc.admin.domain.admin.entity.Administrator;
|
||||
import com.bpgroup.poc.admin.domain.admin.entity.AdministratorRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
public class LoginService {
|
||||
|
||||
private final AdministratorRepository loginRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public LoginResult login(String loginId, String pwd) throws AdministratorNotFoundException, InvalidPasswordException, DoNotHaveAnyMenuException {
|
||||
|
||||
Optional<Administrator> administrator = loginRepository.findByLoginId(loginId);
|
||||
if (administrator.isEmpty()) {
|
||||
throw new AdministratorNotFoundException(loginId);
|
||||
}
|
||||
|
||||
if (!passwordEncoder.matches(pwd, administrator.get().getPassword())) {
|
||||
throw new InvalidPasswordException(loginId);
|
||||
}
|
||||
|
||||
return LoginResult.of(
|
||||
administrator.get().getId(),
|
||||
administrator.get().getLoginId(),
|
||||
administrator.get().getName(),
|
||||
administrator.get().getLoginId(),
|
||||
getMenus(administrator.get())
|
||||
);
|
||||
}
|
||||
|
||||
private static LinkedHashSet<LoginResult.MenuInfo> getMenus(Administrator administrator) throws DoNotHaveAnyMenuException {
|
||||
try {
|
||||
return administrator.getAdministratorRole().getRole().getRoleMenus().stream()
|
||||
.map(roleMenu -> LoginResult.MenuInfo.of(
|
||||
roleMenu.getMenu().getMenuGroup().getUri(),
|
||||
roleMenu.getMenu().getMenuGroup().getName(),
|
||||
roleMenu.getMenu().getMenuGroup().getSortOrder(),
|
||||
roleMenu.getMenu().getUri(),
|
||||
roleMenu.getMenu().getName(),
|
||||
roleMenu.getMenu().getSortOrder()
|
||||
))
|
||||
.sorted(
|
||||
Comparator
|
||||
.comparingInt(LoginResult.MenuInfo::getMenuGroupSortOrder)
|
||||
.thenComparingInt(LoginResult.MenuInfo::getMenuSortOrder)
|
||||
)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
} catch (NullPointerException e) {
|
||||
throw new DoNotHaveAnyMenuException();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
package com.bpgroup.poc.admin.domain.admin.service;
|
||||
package com.bpgroup.poc.admin.domain.admin.command;
|
||||
|
||||
import com.bpgroup.poc.admin.domain.admin.entity.Administrator;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ public class AdministratorCreateCommand {
|
|||
@NotBlank
|
||||
private String name;
|
||||
|
||||
@Builder
|
||||
public static AdministratorCreateCommand of(String loginId, String password, String email, String name) {
|
||||
AdministratorCreateCommand command = new AdministratorCreateCommand();
|
||||
command.loginId = loginId;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.bpgroup.poc.admin.domain.admin.service;
|
||||
package com.bpgroup.poc.admin.domain.admin.command;
|
||||
|
||||
import com.bpgroup.poc.admin.domain.admin.entity.Administrator;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.bpgroup.poc.admin.domain.admin.command;
|
||||
|
||||
import com.bpgroup.poc.admin.domain.admin.entity.Role;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public class RoleCreateCommand {
|
||||
|
||||
@NotBlank
|
||||
private String name;
|
||||
|
||||
private String description;
|
||||
|
||||
@Builder
|
||||
public static RoleCreateCommand of(String name, String description) {
|
||||
RoleCreateCommand command = new RoleCreateCommand();
|
||||
command.name = name;
|
||||
command.description = description;
|
||||
return command;
|
||||
}
|
||||
|
||||
public Role toEntity() {
|
||||
return Role.builder()
|
||||
.name(name)
|
||||
.description(description)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.bpgroup.poc.admin.domain.admin.command;
|
||||
|
||||
import com.bpgroup.poc.admin.domain.admin.entity.Role;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public class RoleUpdateCommand {
|
||||
|
||||
@NotBlank
|
||||
private String name;
|
||||
|
||||
private String description;
|
||||
|
||||
@Builder
|
||||
public static RoleUpdateCommand of(String name, String description) {
|
||||
RoleUpdateCommand command = new RoleUpdateCommand();
|
||||
command.name = name;
|
||||
command.description = description;
|
||||
return command;
|
||||
}
|
||||
|
||||
public Role toEntity() {
|
||||
return Role.builder()
|
||||
.name(name)
|
||||
.description(description)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.bpgroup.poc.admin.domain.admin.entity;
|
|||
|
||||
import com.bpgroup.poc.admin.domain.BaseEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.HashSet;
|
||||
|
|
@ -25,4 +26,17 @@ public class Role extends BaseEntity {
|
|||
@OneToMany(mappedBy = "role", fetch = FetchType.LAZY)
|
||||
private Set<RoleMenu> roleMenus = new HashSet<>();
|
||||
|
||||
@Builder
|
||||
public static Role of(String name, String description) {
|
||||
Role role = new Role();
|
||||
role.name = name;
|
||||
role.description = description;
|
||||
return role;
|
||||
|
||||
}
|
||||
|
||||
public void update(Role updateRole) {
|
||||
this.name = updateRole.name;
|
||||
this.description = updateRole.description;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.bpgroup.poc.admin.domain.admin.service;
|
||||
package com.bpgroup.poc.admin.domain.admin.exception;
|
||||
|
||||
public class DuplicationAdministratorException extends RuntimeException {
|
||||
public DuplicationAdministratorException(String loginId) {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.bpgroup.poc.admin.domain.admin.service;
|
||||
package com.bpgroup.poc.admin.domain.admin.exception;
|
||||
|
||||
import com.bpgroup.poc.admin.domain.DomainException;
|
||||
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
package com.bpgroup.poc.admin.domain.admin.service;
|
||||
|
||||
import com.bpgroup.poc.admin.domain.admin.entity.Administrator;
|
||||
import com.bpgroup.poc.admin.domain.admin.entity.AdministratorRepository;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Validated
|
||||
@Transactional
|
||||
public class AdministratorService {
|
||||
|
||||
private final AdministratorRepository administratorRepository;
|
||||
|
||||
public Long create(
|
||||
@NotNull @Valid AdministratorCreateCommand command
|
||||
) {
|
||||
// 대소문자 구별이 필요한 경우 인증 부분과 유효성 검사 부분 모두 변경이 필요하다.
|
||||
Optional<Administrator> administrator = administratorRepository.findByLoginId(command.getLoginId());
|
||||
if (administrator.isPresent()) {
|
||||
throw new DuplicationAdministratorException(command.getLoginId());
|
||||
}
|
||||
|
||||
Administrator createAdministrator = administratorRepository.save(command.toEntity());
|
||||
return createAdministrator.getId();
|
||||
}
|
||||
|
||||
public void update(
|
||||
@NotNull @Valid AdministratorUpdateCommand command
|
||||
) {
|
||||
Optional<Administrator> administrator = administratorRepository.findById(command.getId());
|
||||
if (administrator.isEmpty()) {
|
||||
throw new NotFoundAdministratorException();
|
||||
}
|
||||
|
||||
administrator.get().update(command.toEntity());
|
||||
}
|
||||
|
||||
public void delete(
|
||||
@NotNull Long id
|
||||
) {
|
||||
administratorRepository.deleteById(id);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,27 @@
|
|||
package com.bpgroup.poc.admin.security;
|
||||
|
||||
import com.bpgroup.poc.admin.common.FormatHelper;
|
||||
import com.bpgroup.poc.admin.security.authentication.AuthenticationFailException;
|
||||
import com.bpgroup.poc.admin.security.authentication.CustomAuthenticationFilter;
|
||||
import com.bpgroup.poc.admin.security.authentication.CustomAuthenticationProvider;
|
||||
import com.bpgroup.poc.admin.security.authentication.service.LoginService;
|
||||
import com.bpgroup.poc.admin.security.jwt.JwtTokenValidatorFilter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
||||
import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
|
|
@ -20,28 +31,32 @@ public class SecurityConfig {
|
|||
private static final String LOGOUT_PATH = "/logout";
|
||||
private static final String ERROR_PATH = "/error";
|
||||
|
||||
private final LoginService loginService;
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
|
||||
http.sessionManagement(t -> t.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
|
||||
|
||||
// 보안 기본 설정
|
||||
http.headers(c -> c
|
||||
.frameOptions(fo -> fo.sameOrigin()) // X-Frame-Options: Same Origin
|
||||
.xssProtection(xp -> xp.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK)) // X-XSS-Protection: 1; mode=block
|
||||
.contentTypeOptions(Customizer.withDefaults()) // X-Content-Type-Options: nosniff
|
||||
.cacheControl(cache -> cache.disable()) //ERR_CACHE_MISS
|
||||
);
|
||||
|
||||
// 인증 설정
|
||||
http.authorizeHttpRequests(c -> c
|
||||
.requestMatchers("/css/**", "/images/**", "/js/**").permitAll()
|
||||
.requestMatchers("/css/**", "/images/**", "/js/**", "/font/**").permitAll()
|
||||
.requestMatchers("/common/modal/**").permitAll()
|
||||
.requestMatchers(LOGIN_PATH, LOGOUT_PATH, ERROR_PATH).permitAll()
|
||||
.anyRequest().authenticated());
|
||||
|
||||
http.formLogin(c -> c
|
||||
.loginPage(LOGIN_PATH)
|
||||
.loginProcessingUrl(LOGIN_PATH)
|
||||
.usernameParameter("loginId")
|
||||
.passwordParameter("password")
|
||||
.defaultSuccessUrl("/main")
|
||||
.failureHandler((req, res, ex) -> {
|
||||
if (Objects.requireNonNull(ex) instanceof AuthenticationFailException authenticationFailException) {
|
||||
res.sendRedirect(FormatHelper.format(LOGIN_PATH + "?error={}", authenticationFailException.getReason()));
|
||||
} else {
|
||||
res.sendRedirect(ERROR_PATH);
|
||||
}
|
||||
}));
|
||||
http.formLogin(AbstractHttpConfigurer::disable); // Form 로그인이 아닌 Json 로그인으로 분리
|
||||
http.addFilterBefore(authenticationGenerateFilter(), UsernamePasswordAuthenticationFilter.class); // 로그인 관련 Filter 설정
|
||||
|
||||
http.addFilterAfter(new JwtTokenValidatorFilter(), BasicAuthenticationFilter.class);
|
||||
|
||||
http.logout(c -> c
|
||||
.logoutRequestMatcher(new AntPathRequestMatcher(LOGOUT_PATH, "GET"))
|
||||
|
|
@ -51,12 +66,23 @@ public class SecurityConfig {
|
|||
return http.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bcrypt Version, Bcrypt Strength, Salt String 설정은 생성자를 이용하여 설정 가능
|
||||
*/
|
||||
@Bean
|
||||
public BCryptPasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
public CustomAuthenticationProvider customAuthenticationProvider() {
|
||||
return new CustomAuthenticationProvider(loginService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager() {
|
||||
List<AuthenticationProvider> providers = new ArrayList<>();
|
||||
providers.add(customAuthenticationProvider());
|
||||
return new ProviderManager(providers);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CustomAuthenticationFilter authenticationGenerateFilter() {
|
||||
CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
|
||||
filter.setAuthenticationManager(authenticationManager());
|
||||
return filter;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
package com.bpgroup.poc.admin.security;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
|
||||
/**
|
||||
* SecurityConfig 에 있을 경우 Service 에 주입 시 순환 참조가 발생하여 별로 파일로 분리
|
||||
*/
|
||||
@Configuration
|
||||
public class SecurityEncryptConfig {
|
||||
/**
|
||||
* Bcrypt Version, Bcrypt Strength, Salt String 설정은 생성자를 이용하여 설정 가능
|
||||
*/
|
||||
@Bean
|
||||
public BCryptPasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.bpgroup.poc.admin.security;
|
||||
|
||||
public class SecurityFilterConstants {
|
||||
public static final String[] EXCLUDE_FILTER_STARTS_WITH_URI = {"/login", "/logout", "/error", "/css", "/js", "/images", "/favicon.ico", "/common/modal", "/font"};
|
||||
}
|
||||
|
|
@ -1,87 +1,41 @@
|
|||
package com.bpgroup.poc.admin.security.authentication;
|
||||
|
||||
import com.bpgroup.poc.admin.app.login.LoginResult;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
public class AuthenticationDetail {
|
||||
|
||||
private Long id;
|
||||
private String loginId;
|
||||
private String name;
|
||||
private String email;
|
||||
|
||||
private MultiValueMap<MenuOneDepth, MenuTwoDepth> menus = new LinkedMultiValueMap<>();
|
||||
|
||||
public static AuthenticationDetail from(LoginResult result) {
|
||||
@Builder
|
||||
public static AuthenticationDetail of(String loginId, String name, String email) {
|
||||
AuthenticationDetail authenticationDetail = new AuthenticationDetail();
|
||||
authenticationDetail.id = result.getId();
|
||||
authenticationDetail.loginId = result.getLoginId();
|
||||
authenticationDetail.name = result.getName();
|
||||
authenticationDetail.email = result.getEmail();
|
||||
|
||||
result.getMenus().forEach(menu -> authenticationDetail.menus.add(
|
||||
MenuOneDepth.of(
|
||||
menu.getMenuGroupUri(),
|
||||
menu.getMenuGroupName(),
|
||||
menu.getMenuGroupSortOrder()
|
||||
),
|
||||
MenuTwoDepth.of(
|
||||
menu.getMenuUri(),
|
||||
menu.getMenuName(),
|
||||
menu.getMenuSortOrder()
|
||||
)
|
||||
));
|
||||
|
||||
authenticationDetail.loginId = loginId;
|
||||
authenticationDetail.name = name;
|
||||
authenticationDetail.email = email;
|
||||
return authenticationDetail;
|
||||
}
|
||||
|
||||
/**
|
||||
* EqualsAndHashCode annotattion이 없을 경우 MultiValueMap을 사용할 때 중복된 key를 찾지 못함
|
||||
*/
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
private static class MenuOneDepth {
|
||||
private String uri;
|
||||
private String name;
|
||||
private Integer sortOrder;
|
||||
|
||||
public static MenuOneDepth of(
|
||||
String uri,
|
||||
String name,
|
||||
Integer sortOrder
|
||||
) {
|
||||
MenuOneDepth menu = new MenuOneDepth();
|
||||
menu.uri = uri;
|
||||
menu.name = name;
|
||||
menu.sortOrder = sortOrder;
|
||||
return menu;
|
||||
public String toJsonString() {
|
||||
try {
|
||||
return new ObjectMapper().writeValueAsString(this);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
private static class MenuTwoDepth {
|
||||
private String uri;
|
||||
private String name;
|
||||
private Integer sortOrder;
|
||||
|
||||
public static MenuTwoDepth of(
|
||||
String uri,
|
||||
String name,
|
||||
Integer sortOrder
|
||||
) {
|
||||
MenuTwoDepth menu = new MenuTwoDepth();
|
||||
menu.uri = uri;
|
||||
menu.name = name;
|
||||
menu.sortOrder = sortOrder;
|
||||
return menu;
|
||||
public static AuthenticationDetail fromJsonString(String jsonString) {
|
||||
try {
|
||||
return new ObjectMapper().readValue(jsonString, AuthenticationDetail.class);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,26 @@
|
|||
package com.bpgroup.poc.admin.security.authentication;
|
||||
|
||||
import com.bpgroup.poc.admin.app.login.exception.AdministratorNotFoundException;
|
||||
import com.bpgroup.poc.admin.app.login.exception.DoNotHaveAnyMenuException;
|
||||
import com.bpgroup.poc.admin.app.login.exception.InvalidPasswordException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.bpgroup.poc.admin.security.authentication.service.exception.AdministratorNotFoundException;
|
||||
import com.bpgroup.poc.admin.security.authentication.service.exception.DoNotHaveAnyMenuException;
|
||||
import com.bpgroup.poc.admin.security.authentication.service.exception.InvalidPasswordException;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Slf4j
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum AuthenticationFailReason {
|
||||
WRONG_LOGIN_ID,
|
||||
WRONG_PASSWORD,
|
||||
HAVE_NO_MENU,
|
||||
INTERNAL_ERROR;
|
||||
WRONG_LOGIN_ID("아이디 및 패스워드를 확인하세요."),
|
||||
WRONG_PASSWORD("아이디 및 패스워드를 확인하세요."),
|
||||
HAVE_NO_MENU("등록된 메뉴 권한이 없습니다. \n메뉴 등록 후 사용하시기 바랍니다."),
|
||||
INTERNAL_ERROR("서버 내부 오류가 발생했습니다.");
|
||||
|
||||
private final String message;
|
||||
|
||||
public static AuthenticationFailReason from(Exception e) {
|
||||
if (e instanceof AdministratorNotFoundException || e instanceof InvalidPasswordException) {
|
||||
if (e instanceof AdministratorNotFoundException) {
|
||||
return WRONG_LOGIN_ID;
|
||||
} else if (e instanceof InvalidPasswordException) {
|
||||
return WRONG_PASSWORD;
|
||||
} else if (e instanceof DoNotHaveAnyMenuException) {
|
||||
return HAVE_NO_MENU;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
package com.bpgroup.poc.admin.security.authentication;
|
||||
|
||||
import com.bpgroup.poc.admin.security.authentication.service.LoginResponse;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
|
||||
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
|
||||
@Override
|
||||
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
|
||||
response.setStatus(HttpServletResponse.SC_OK);
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
String failMessage = "시스템 오류가 발생했습니다.";
|
||||
|
||||
if (Objects.requireNonNull(exception) instanceof AuthenticationFailException authenticationFailException) {
|
||||
failMessage = authenticationFailException.getReason().getMessage();
|
||||
}
|
||||
|
||||
String jsonResponse = new ObjectMapper().writeValueAsString(
|
||||
LoginResponse.fail(
|
||||
"9999",
|
||||
failMessage
|
||||
)
|
||||
);
|
||||
response.getWriter().write(jsonResponse);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.bpgroup.poc.admin.security.authentication;
|
||||
|
||||
import com.bpgroup.poc.admin.security.authentication.service.LoginRequest;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class CustomAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
|
||||
private static final String DEFAULT_LOGIN_REQUEST_URL = "/login";
|
||||
private static final String HTTP_METHOD = "POST";
|
||||
private static final String CONTENT_TYPE = "application/json";
|
||||
private boolean postOnly = true;
|
||||
|
||||
public CustomAuthenticationFilter() {
|
||||
super(new AntPathRequestMatcher(DEFAULT_LOGIN_REQUEST_URL, HTTP_METHOD)); // 위에서 설정한 /oauth2/login/* 의 요청에, GET으로 온 요청을 처리하기 위해 설정한다.
|
||||
setAuthenticationSuccessHandler(new CustomAuthenticationSuccessHandler());
|
||||
setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
|
||||
// 요청에 대한 유효성 검사
|
||||
isValidated(request);
|
||||
|
||||
LoginRequest loginRequest = new ObjectMapper().readValue(
|
||||
StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8),
|
||||
LoginRequest.class
|
||||
);
|
||||
|
||||
String username = loginRequest.getUsername();
|
||||
String password = loginRequest.getPassword();
|
||||
|
||||
if (username == null || password == null) {
|
||||
throw new AuthenticationServiceException("DATA IS MISS");
|
||||
}
|
||||
|
||||
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
|
||||
setDetails(request, authenticationToken);
|
||||
|
||||
return this.getAuthenticationManager().authenticate(authenticationToken);
|
||||
}
|
||||
|
||||
private void isValidated(HttpServletRequest request) {
|
||||
if (this.postOnly && !request.getMethod().equals(HTTP_METHOD)) {
|
||||
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
|
||||
}
|
||||
|
||||
if (request.getContentType() == null || !request.getContentType().equals(CONTENT_TYPE)) {
|
||||
throw new AuthenticationServiceException("Authentication Content-Type not supported: " + request.getContentType());
|
||||
}
|
||||
}
|
||||
|
||||
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
|
||||
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,27 +1,28 @@
|
|||
package com.bpgroup.poc.admin.security.authentication;
|
||||
|
||||
import com.bpgroup.poc.admin.app.login.LoginResult;
|
||||
import com.bpgroup.poc.admin.app.login.LoginService;
|
||||
import com.bpgroup.poc.admin.security.authentication.service.LoginResult;
|
||||
import com.bpgroup.poc.admin.security.authentication.service.LoginService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CustomAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private final LoginService loginService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
String loginId = (String) authentication.getPrincipal();
|
||||
String password = (String) authentication.getCredentials();
|
||||
|
||||
try {
|
||||
LoginResult loginResult = loginService.login(loginId, password);
|
||||
String username = (String) authentication.getPrincipal();
|
||||
String password = (String) authentication.getCredentials();
|
||||
|
||||
LoginResult loginResult = loginService.login(username, password);
|
||||
|
||||
return buildAuthenticationToken(loginResult);
|
||||
} catch (Exception e) {
|
||||
throw new AuthenticationFailException("로그인에 실패하였습니다.", AuthenticationFailReason.from(e));
|
||||
|
|
@ -29,13 +30,22 @@ public class CustomAuthenticationProvider implements AuthenticationProvider {
|
|||
}
|
||||
|
||||
private UsernamePasswordAuthenticationToken buildAuthenticationToken(LoginResult result) {
|
||||
UsernamePasswordAuthenticationToken authenticationToken = UsernamePasswordAuthenticationToken.authenticated(
|
||||
UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated(
|
||||
result.getLoginId(),
|
||||
null,
|
||||
null
|
||||
);
|
||||
authenticationToken.setDetails(AuthenticationDetail.from(result));
|
||||
return authenticationToken;
|
||||
|
||||
token.setDetails(
|
||||
AuthenticationDetail.builder()
|
||||
.loginId(result.getLoginId())
|
||||
.name(result.getName())
|
||||
.email(result.getEmail())
|
||||
.build()
|
||||
.toJsonString()
|
||||
);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
package com.bpgroup.poc.admin.security.authentication;
|
||||
|
||||
import com.bpgroup.poc.admin.security.authentication.service.LoginResponse;
|
||||
import com.bpgroup.poc.admin.security.jwt.JwtConstants;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
|
||||
@Override
|
||||
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
|
||||
response.setStatus(HttpServletResponse.SC_OK);
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
|
||||
// JWT 토큰을 쿠키에 추가
|
||||
String jwtToken = createJwtToken(authentication);
|
||||
Cookie jwtCookie = new Cookie(JwtConstants.JWT_TOKEN_NAME, jwtToken);
|
||||
jwtCookie.setHttpOnly(true);
|
||||
jwtCookie.setPath("/");
|
||||
response.addCookie(jwtCookie);
|
||||
|
||||
String jsonResponse = new ObjectMapper().writeValueAsString(
|
||||
LoginResponse.success()
|
||||
);
|
||||
|
||||
response.getWriter().write(jsonResponse);
|
||||
}
|
||||
|
||||
private static String createJwtToken(Authentication authentication) {
|
||||
SecretKey key = Keys.hmacShaKeyFor(JwtConstants.JWT_KEY.getBytes(StandardCharsets.UTF_8));
|
||||
return Jwts.builder()
|
||||
.issuer("BP Admin")
|
||||
.subject("Jwt Token")
|
||||
.claim("username", authentication.getName())
|
||||
.claim("details", authentication.getDetails())
|
||||
.issuedAt(new java.util.Date())
|
||||
.expiration(new java.util.Date(System.currentTimeMillis() + JwtConstants.EXPIRATION_TIME))
|
||||
.signWith(key)
|
||||
.compact();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.bpgroup.poc.admin.security.authentication.service;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 로그인 요청 Request
|
||||
*/
|
||||
@Data
|
||||
public class LoginRequest {
|
||||
private String username;
|
||||
private String password;
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.bpgroup.poc.admin.security.authentication.service;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public class LoginResponse {
|
||||
|
||||
private String resultCode;
|
||||
private String resultMessage;
|
||||
private String token;
|
||||
|
||||
public static LoginResponse success() {
|
||||
LoginResponse response = new LoginResponse();
|
||||
response.resultCode = "0000";
|
||||
response.resultMessage = "Success";
|
||||
return response;
|
||||
}
|
||||
|
||||
public static LoginResponse fail(String resultCode, String resultMessage) {
|
||||
LoginResponse response = new LoginResponse();
|
||||
response.resultCode = resultCode;
|
||||
response.resultMessage = resultMessage;
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.bpgroup.poc.admin.security.authentication.service;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class LoginResult {
|
||||
private Long id;
|
||||
private String loginId;
|
||||
private String name;
|
||||
private String email;
|
||||
|
||||
public static LoginResult of(Long id, String loginId, String name, String email) {
|
||||
LoginResult loginResult = new LoginResult();
|
||||
loginResult.id = id;
|
||||
loginResult.loginId = loginId;
|
||||
loginResult.name = name;
|
||||
loginResult.email = email;
|
||||
return loginResult;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.bpgroup.poc.admin.security.authentication.service;
|
||||
|
||||
import com.bpgroup.poc.admin.domain.admin.entity.Administrator;
|
||||
import com.bpgroup.poc.admin.domain.admin.entity.AdministratorRepository;
|
||||
import com.bpgroup.poc.admin.security.authentication.service.exception.AdministratorNotFoundException;
|
||||
import com.bpgroup.poc.admin.security.authentication.service.exception.DoNotHaveAnyMenuException;
|
||||
import com.bpgroup.poc.admin.security.authentication.service.exception.InvalidPasswordException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LoginService {
|
||||
|
||||
private final AdministratorRepository administratorRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@Transactional
|
||||
public LoginResult login(String username, String password) throws AdministratorNotFoundException, InvalidPasswordException, DoNotHaveAnyMenuException {
|
||||
Optional<Administrator> administrator = administratorRepository.findByLoginId(username);
|
||||
if (administrator.isEmpty()) {
|
||||
throw new AdministratorNotFoundException(username);
|
||||
}
|
||||
|
||||
if (!passwordEncoder.matches(password, administrator.get().getPassword())) {
|
||||
throw new InvalidPasswordException(username);
|
||||
}
|
||||
|
||||
return LoginResult.of(
|
||||
administrator.get().getId(),
|
||||
administrator.get().getLoginId(),
|
||||
administrator.get().getName(),
|
||||
administrator.get().getLoginId()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.bpgroup.poc.admin.app.login.exception;
|
||||
package com.bpgroup.poc.admin.security.authentication.service.exception;
|
||||
|
||||
public class AdministratorNotFoundException extends Exception {
|
||||
public AdministratorNotFoundException(String message) {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.bpgroup.poc.admin.app.login.exception;
|
||||
package com.bpgroup.poc.admin.security.authentication.service.exception;
|
||||
|
||||
public class DoNotHaveAnyMenuException extends Exception {
|
||||
public DoNotHaveAnyMenuException() {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.bpgroup.poc.admin.app.login.exception;
|
||||
package com.bpgroup.poc.admin.security.authentication.service.exception;
|
||||
|
||||
public class InvalidPasswordException extends Exception {
|
||||
public InvalidPasswordException(String message) {
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.bpgroup.poc.admin.security.jwt;
|
||||
|
||||
/**
|
||||
* TODO: 사용 시 별도 property 파일로 관리 필요
|
||||
*/
|
||||
public class JwtConstants {
|
||||
public static final String JWT_KEY = "8530b13adb4e420d9694b27570635b47";
|
||||
public static final String JWT_TOKEN_NAME = "JWT-TOKEN";
|
||||
public static final long EXPIRATION_TIME = 60 * 10 * 1000;
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package com.bpgroup.poc.admin.security.jwt;
|
||||
|
||||
import com.bpgroup.poc.admin.security.SecurityFilterConstants;
|
||||
import com.bpgroup.poc.admin.security.authentication.AuthenticationDetail;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
public class JwtTokenValidatorFilter extends OncePerRequestFilter {
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||
String jwt = getJwtFromCookie(request);
|
||||
if (null != jwt) {
|
||||
try {
|
||||
SecretKey key = Keys.hmacShaKeyFor(
|
||||
JwtConstants.JWT_KEY.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
Claims claims = Jwts.parser()
|
||||
.verifyWith(key)
|
||||
.build()
|
||||
.parseSignedClaims(jwt)
|
||||
.getPayload();
|
||||
String username = claims.get("username", String.class);
|
||||
String details = claims.get("details", String.class);
|
||||
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null, null);
|
||||
auth.setDetails(AuthenticationDetail.fromJsonString(details));
|
||||
SecurityContextHolder.getContext().setAuthentication(auth);
|
||||
} catch (Exception e) {
|
||||
throw new BadCredentialsException("Invalid Jwt Token");
|
||||
}
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private static String getJwtFromCookie(HttpServletRequest request) {
|
||||
Cookie[] cookies = request.getCookies();
|
||||
if (null != cookies) {
|
||||
for (Cookie cookie : cookies) {
|
||||
if (JwtConstants.JWT_TOKEN_NAME.equals(cookie.getName())) {
|
||||
return cookie.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
List<String> paths = List.of(SecurityFilterConstants.EXCLUDE_FILTER_STARTS_WITH_URI);
|
||||
return paths.stream().anyMatch(request.getRequestURI()::startsWith);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
package com.bpgroup.poc.admin.values;
|
||||
|
||||
public class AdministratorMenu {
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.bpgroup.poc.admin.web.advice.menu;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
public class MenuInfo {
|
||||
|
||||
private String uri;
|
||||
private String name;
|
||||
private Integer sortOrder;
|
||||
|
||||
List<MenuChild> menuChildren = new ArrayList<>();
|
||||
|
||||
@Builder
|
||||
public static MenuInfo of(String uri, String name, Integer sortOrder) {
|
||||
MenuInfo menuInfo = new MenuInfo();
|
||||
menuInfo.uri = uri;
|
||||
menuInfo.name = name;
|
||||
menuInfo.sortOrder = sortOrder;
|
||||
return menuInfo;
|
||||
}
|
||||
|
||||
public void addMenuChild(MenuChild menuChild) {
|
||||
menuChildren.add(menuChild);
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@Builder
|
||||
public static class MenuChild {
|
||||
private String uri;
|
||||
private String name;
|
||||
private Integer sortOrder;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.bpgroup.poc.admin.web.advice.menu;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ControllerAdvice(basePackages = "com.bpgroup.poc.admin.web.main")
|
||||
@RequiredArgsConstructor
|
||||
public class MenuInfoControllerAdvice {
|
||||
|
||||
private final MenuQueryRepository menuQueryRepository;
|
||||
|
||||
@ModelAttribute("menuInfos")
|
||||
public List<MenuInfo> menuInfo() {
|
||||
return menuQueryRepository.findAllByLoginId();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package com.bpgroup.poc.admin.web.advice.menu;
|
||||
|
||||
import com.querydsl.core.Tuple;
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static com.bpgroup.poc.admin.domain.admin.entity.QAdministrator.administrator;
|
||||
import static com.bpgroup.poc.admin.domain.admin.entity.QAdministratorRole.administratorRole;
|
||||
import static com.bpgroup.poc.admin.domain.admin.entity.QMenu.menu;
|
||||
import static com.bpgroup.poc.admin.domain.admin.entity.QMenuGroup.menuGroup;
|
||||
import static com.bpgroup.poc.admin.domain.admin.entity.QRole.role;
|
||||
import static com.bpgroup.poc.admin.domain.admin.entity.QRoleMenu.roleMenu;
|
||||
|
||||
@Repository
|
||||
@RequiredArgsConstructor
|
||||
public class MenuQueryRepository {
|
||||
|
||||
private final JPAQueryFactory queryFactory;
|
||||
|
||||
public List<MenuInfo> findAllByLoginId() {
|
||||
List<Tuple> results = queryFactory.select(
|
||||
menuGroup.uri,
|
||||
menuGroup.name,
|
||||
menuGroup.sortOrder,
|
||||
roleMenu.menu.uri,
|
||||
roleMenu.menu.name,
|
||||
roleMenu.menu.sortOrder
|
||||
)
|
||||
.from(administrator)
|
||||
.innerJoin(administrator.administratorRole, administratorRole)
|
||||
.innerJoin(administratorRole.role, role)
|
||||
.innerJoin(role.roleMenus, roleMenu)
|
||||
.innerJoin(roleMenu.menu, menu)
|
||||
.innerJoin(menu.menuGroup, menuGroup)
|
||||
.where(administrator.loginId.eq("admin"))
|
||||
.orderBy(menuGroup.sortOrder.asc(), roleMenu.menu.sortOrder.asc())
|
||||
.fetch();
|
||||
|
||||
LinkedHashMap<String, MenuInfo> menuInfoMap = makeMenuForm(results);
|
||||
return new ArrayList<>(menuInfoMap.values());
|
||||
}
|
||||
|
||||
private static LinkedHashMap<String, MenuInfo> makeMenuForm(List<Tuple> results) {
|
||||
LinkedHashMap<String, MenuInfo> menuInfoMap = new LinkedHashMap<>();
|
||||
|
||||
for (Tuple tuple : results) {
|
||||
String menuGroupUri = tuple.get(menuGroup.uri);
|
||||
MenuInfo menuInfo = menuInfoMap.get(menuGroupUri);
|
||||
|
||||
if (menuInfo == null) {
|
||||
menuInfo = MenuInfo.builder()
|
||||
.uri(tuple.get(menuGroup.uri))
|
||||
.name(tuple.get(menuGroup.name))
|
||||
.sortOrder(tuple.get(menuGroup.sortOrder))
|
||||
.build();
|
||||
menuInfoMap.put(menuGroupUri, menuInfo);
|
||||
}
|
||||
|
||||
MenuInfo.MenuChild menuChild = MenuInfo.MenuChild.builder()
|
||||
.uri(tuple.get(roleMenu.menu.uri))
|
||||
.name(tuple.get(roleMenu.menu.name))
|
||||
.sortOrder(tuple.get(roleMenu.menu.sortOrder))
|
||||
.build();
|
||||
|
||||
menuInfo.addMenuChild(menuChild);
|
||||
}
|
||||
|
||||
return menuInfoMap;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.bpgroup.poc.admin.web.advice;
|
||||
package com.bpgroup.poc.admin.web.advice.path;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.Getter;
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
package com.bpgroup.poc.admin.web.common;
|
||||
|
||||
public class CommonResponse {
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class CommonResponse {
|
||||
protected String resultCode;
|
||||
protected String resultMessage;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +1,16 @@
|
|||
package com.bpgroup.poc.admin.web.login;
|
||||
|
||||
import com.bpgroup.poc.admin.security.authentication.AuthenticationFailReason;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/login")
|
||||
public class LoginController {
|
||||
|
||||
@GetMapping
|
||||
public String loginPage(
|
||||
@RequestParam(required = false) AuthenticationFailReason error,
|
||||
Model model
|
||||
) {
|
||||
if (error != null) {
|
||||
model.addAttribute("errorMessage", getMessage(error));
|
||||
}
|
||||
|
||||
public String loginPage() {
|
||||
return "login/login";
|
||||
}
|
||||
|
||||
private String getMessage(AuthenticationFailReason error) {
|
||||
return switch (error) {
|
||||
case WRONG_LOGIN_ID, WRONG_PASSWORD -> "아이디 또는 비밀번호가 일치하지 않습니다.";
|
||||
case HAVE_NO_MENU -> "등록된 메뉴가 없습니다.\n 메뉴 등록 후 이용해주세요.";
|
||||
default -> "서버에 오류가 발생했습니다.";
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.bpgroup.poc.admin.web.main.admin.management.reqres.AdministratorUpdat
|
|||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
|
@ -18,7 +19,8 @@ import java.util.List;
|
|||
@RequestMapping("/admin/management")
|
||||
public class AdministratorManagementRestController {
|
||||
|
||||
private final AdministratorManagementWebService administratorManagementWebService;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final AdministratorManagementService administratorManagementService;
|
||||
|
||||
/**
|
||||
* 전체 조회
|
||||
|
|
@ -26,7 +28,7 @@ public class AdministratorManagementRestController {
|
|||
*/
|
||||
@GetMapping("/list")
|
||||
public ResponseEntity<?> getAdministrators() {
|
||||
List<AdministratorFind.Response> response = administratorManagementWebService.findAll();
|
||||
List<AdministratorFind.Response> response = administratorManagementService.findAll();
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
|
|
@ -37,7 +39,7 @@ public class AdministratorManagementRestController {
|
|||
*/
|
||||
@GetMapping("/{loginId}")
|
||||
public ResponseEntity<?> getAdministrator(@PathVariable @NotBlank String loginId) {
|
||||
AdministratorFind.Response response = administratorManagementWebService.find(loginId);
|
||||
AdministratorFind.Response response = administratorManagementService.find(loginId);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +54,8 @@ public class AdministratorManagementRestController {
|
|||
@RequestBody @Validated AdministratorCreate.Request request,
|
||||
BindingResult bindingResult
|
||||
) {
|
||||
AdministratorCreate.Response response = administratorManagementWebService.create(request);
|
||||
request.setPassword(passwordEncoder.encode(request.getPassword()));
|
||||
AdministratorCreate.Response response = administratorManagementService.create(request);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
|
|
@ -61,8 +64,9 @@ public class AdministratorManagementRestController {
|
|||
@RequestBody @Validated AdministratorUpdate.Request request,
|
||||
BindingResult bindingResult
|
||||
) {
|
||||
administratorManagementWebService.update(request);
|
||||
return ResponseEntity.ok().build();
|
||||
request.setPassword(passwordEncoder.encode(request.getPassword()));
|
||||
AdministratorUpdate.Response response = administratorManagementService.update(request);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -76,8 +80,8 @@ public class AdministratorManagementRestController {
|
|||
@RequestBody @Validated AdministratorDelete.Request request,
|
||||
BindingResult bindingResult
|
||||
) {
|
||||
administratorManagementWebService.delete(request);
|
||||
return ResponseEntity.ok().build();
|
||||
AdministratorDelete.Response response = administratorManagementService.delete(request);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
package com.bpgroup.poc.admin.web.main.admin.management;
|
||||
|
||||
import com.bpgroup.poc.admin.domain.admin.command.AdministratorCreateCommand;
|
||||
import com.bpgroup.poc.admin.domain.admin.command.AdministratorUpdateCommand;
|
||||
import com.bpgroup.poc.admin.domain.admin.entity.Administrator;
|
||||
import com.bpgroup.poc.admin.domain.admin.entity.AdministratorRepository;
|
||||
import com.bpgroup.poc.admin.web.main.admin.management.reqres.AdministratorCreate;
|
||||
import com.bpgroup.poc.admin.web.main.admin.management.reqres.AdministratorDelete;
|
||||
import com.bpgroup.poc.admin.web.main.admin.management.reqres.AdministratorFind;
|
||||
import com.bpgroup.poc.admin.web.main.admin.management.reqres.AdministratorUpdate;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
public class AdministratorManagementService {
|
||||
|
||||
private final AdministratorManagementQueryRepository queryRepository;
|
||||
private final AdministratorRepository repository;
|
||||
|
||||
public List<AdministratorFind.Response> findAll() {
|
||||
return queryRepository.findAll();
|
||||
}
|
||||
|
||||
public AdministratorFind.Response find(@NotBlank String loginId) {
|
||||
return queryRepository.findByLoginId(loginId);
|
||||
}
|
||||
|
||||
public AdministratorCreate.Response create(AdministratorCreate.Request request) {
|
||||
Administrator admin = repository.save(
|
||||
AdministratorCreateCommand.builder()
|
||||
.loginId(request.getLoginId())
|
||||
.password(request.getPassword())
|
||||
.email(request.getEmail())
|
||||
.name(request.getName())
|
||||
.build()
|
||||
.toEntity()
|
||||
);
|
||||
|
||||
return AdministratorCreate.Response.success(admin.getId());
|
||||
}
|
||||
|
||||
public AdministratorUpdate.Response update(AdministratorUpdate.Request request) {
|
||||
Optional<Administrator> findAdministrator = repository.findById(request.getId());
|
||||
if (findAdministrator.isEmpty()) {
|
||||
return AdministratorUpdate.Response.fail("ADMINISTRATOR_NOT_FOUND");
|
||||
}
|
||||
|
||||
Administrator administrator = findAdministrator.get();
|
||||
administrator.update(
|
||||
AdministratorUpdateCommand.of(
|
||||
request.getId(),
|
||||
request.getPassword(),
|
||||
request.getEmail(),
|
||||
request.getName()
|
||||
).toEntity()
|
||||
);
|
||||
|
||||
return AdministratorUpdate.Response.success();
|
||||
}
|
||||
|
||||
public AdministratorDelete.Response delete(AdministratorDelete.Request request) {
|
||||
repository.deleteById(request.getId());
|
||||
return AdministratorDelete.Response.success();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
package com.bpgroup.poc.admin.web.main.admin.management;
|
||||
|
||||
import com.bpgroup.poc.admin.domain.admin.service.AdministratorCreateCommand;
|
||||
import com.bpgroup.poc.admin.domain.admin.service.AdministratorService;
|
||||
import com.bpgroup.poc.admin.domain.admin.service.AdministratorUpdateCommand;
|
||||
import com.bpgroup.poc.admin.web.main.admin.management.reqres.AdministratorCreate;
|
||||
import com.bpgroup.poc.admin.web.main.admin.management.reqres.AdministratorDelete;
|
||||
import com.bpgroup.poc.admin.web.main.admin.management.reqres.AdministratorFind;
|
||||
import com.bpgroup.poc.admin.web.main.admin.management.reqres.AdministratorUpdate;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
public class AdministratorManagementWebService {
|
||||
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
private final AdministratorService administratorService;
|
||||
private final AdministratorManagementQueryRepository administratorManagementQueryRepository;
|
||||
|
||||
public AdministratorCreate.Response create(AdministratorCreate.Request request) {
|
||||
Long id = administratorService.create(AdministratorCreateCommand.of(
|
||||
request.getLoginId(),
|
||||
passwordEncoder.encode(request.getPassword()),
|
||||
request.getEmail(),
|
||||
request.getName()
|
||||
));
|
||||
|
||||
return AdministratorCreate.Response.builder()
|
||||
.id(id)
|
||||
.build();
|
||||
}
|
||||
|
||||
public AdministratorFind.Response find(@NotBlank String loginId) {
|
||||
return administratorManagementQueryRepository.findByLoginId(loginId);
|
||||
}
|
||||
|
||||
public void delete(AdministratorDelete.Request request) {
|
||||
administratorService.delete(request.getId());
|
||||
}
|
||||
|
||||
public void update(AdministratorUpdate.Request request) {
|
||||
administratorService.update(AdministratorUpdateCommand.of(
|
||||
request.getId(),
|
||||
passwordEncoder.encode(request.getPassword()),
|
||||
request.getEmail(),
|
||||
request.getName()
|
||||
));
|
||||
}
|
||||
|
||||
public List<AdministratorFind.Response> findAll() {
|
||||
return administratorManagementQueryRepository.findAll();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
package com.bpgroup.poc.admin.web.main.admin.management.reqres;
|
||||
|
||||
import com.bpgroup.poc.admin.web.common.CommonResponse;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
public class AdministratorDelete {
|
||||
|
||||
|
|
@ -11,4 +15,24 @@ public class AdministratorDelete {
|
|||
private Long id;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public static class Response extends CommonResponse {
|
||||
@Builder
|
||||
public static AdministratorDelete.Response success() {
|
||||
AdministratorDelete.Response response = new AdministratorDelete.Response();
|
||||
response.resultCode = "0000";
|
||||
response.resultMessage = "Success";
|
||||
return response;
|
||||
}
|
||||
|
||||
@Builder
|
||||
public static AdministratorDelete.Response fail(String resultMessage) {
|
||||
AdministratorDelete.Response response = new AdministratorDelete.Response();
|
||||
response.resultCode = "9999";
|
||||
response.resultMessage = resultMessage;
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
package com.bpgroup.poc.admin.web.main.admin.management.reqres;
|
||||
|
||||
import com.bpgroup.poc.admin.web.common.CommonResponse;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
public class AdministratorUpdate {
|
||||
|
||||
|
|
@ -24,4 +28,23 @@ public class AdministratorUpdate {
|
|||
private String name;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public static class Response extends CommonResponse {
|
||||
@Builder
|
||||
public static AdministratorUpdate.Response success() {
|
||||
AdministratorUpdate.Response response = new AdministratorUpdate.Response();
|
||||
response.resultCode = "0000";
|
||||
response.resultMessage = "Success";
|
||||
return response;
|
||||
}
|
||||
|
||||
@Builder
|
||||
public static AdministratorUpdate.Response fail(String resultMessage) {
|
||||
AdministratorUpdate.Response response = new AdministratorUpdate.Response();
|
||||
response.resultCode = "9999";
|
||||
response.resultMessage = resultMessage;
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package com.bpgroup.poc.admin.web.main.admin.role;
|
||||
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
@RequiredArgsConstructor
|
||||
public class RoleQueryRepository {
|
||||
|
||||
private final JPAQueryFactory queryFactory;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.bpgroup.poc.admin.web.main.admin.role;
|
||||
|
||||
import com.bpgroup.poc.admin.web.main.admin.role.reqres.RoleCreate;
|
||||
import com.bpgroup.poc.admin.web.main.admin.role.reqres.RoleDelete;
|
||||
import com.bpgroup.poc.admin.web.main.admin.role.reqres.RoleUpdate;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/admin/role")
|
||||
public class RoleRestController {
|
||||
|
||||
private final RoleService roleService;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 권한 등록
|
||||
*/
|
||||
@PostMapping("/create")
|
||||
public ResponseEntity<?> createRole(
|
||||
@RequestBody @Valid RoleCreate.Request request,
|
||||
BindingResult bindingResult
|
||||
) {
|
||||
RoleCreate.Response response = roleService.create(request);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 권한 수정
|
||||
*/
|
||||
@PostMapping("/update")
|
||||
public ResponseEntity<?> updateRole(
|
||||
@RequestBody @Valid RoleUpdate.Request request,
|
||||
BindingResult bindingResult
|
||||
) {
|
||||
RoleUpdate.Response response = roleService.update(request);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 권한 삭제
|
||||
*/
|
||||
@PostMapping("/delete")
|
||||
public ResponseEntity<?> deleteRole(
|
||||
@RequestBody @Valid RoleDelete.Request request,
|
||||
BindingResult bindingResult
|
||||
) {
|
||||
RoleDelete.Response response = roleService.delete(request);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.bpgroup.poc.admin.web.main.admin.role;
|
||||
|
||||
import com.bpgroup.poc.admin.domain.admin.entity.Role;
|
||||
import com.bpgroup.poc.admin.domain.admin.entity.RoleRepository;
|
||||
import com.bpgroup.poc.admin.domain.admin.command.RoleCreateCommand;
|
||||
import com.bpgroup.poc.admin.domain.admin.command.RoleUpdateCommand;
|
||||
import com.bpgroup.poc.admin.web.main.admin.role.reqres.RoleCreate;
|
||||
import com.bpgroup.poc.admin.web.main.admin.role.reqres.RoleDelete;
|
||||
import com.bpgroup.poc.admin.web.main.admin.role.reqres.RoleUpdate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RoleService {
|
||||
|
||||
private final RoleRepository repository;
|
||||
private final RoleQueryRepository queryRepository;
|
||||
|
||||
/**
|
||||
* ROLE 생성
|
||||
*/
|
||||
public RoleCreate.Response create(RoleCreate.Request request) {
|
||||
Role role = repository.save(
|
||||
RoleCreateCommand.builder()
|
||||
.name(request.getName())
|
||||
.description(request.getDescription())
|
||||
.build()
|
||||
.toEntity()
|
||||
);
|
||||
|
||||
return RoleCreate.Response.success(role.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* ROLE 수정
|
||||
*/
|
||||
public RoleUpdate.Response update(RoleUpdate.Request request) {
|
||||
Optional<Role> role = repository.findById(request.getId());
|
||||
if (role.isEmpty()) {
|
||||
return RoleUpdate.Response.fail("ROLE_NOT_FOUND");
|
||||
}
|
||||
|
||||
Role findRole = role.get();
|
||||
Role updateRole = RoleUpdateCommand.builder()
|
||||
.name(request.getName())
|
||||
.description(request.getDescription())
|
||||
.build()
|
||||
.toEntity();
|
||||
|
||||
findRole.update(updateRole);
|
||||
|
||||
return RoleUpdate.Response.success();
|
||||
}
|
||||
|
||||
public RoleDelete.Response delete(RoleDelete.Request request) {
|
||||
try {
|
||||
repository.deleteById(request.getId());
|
||||
return RoleDelete.Response.success();
|
||||
} catch (Exception e) {
|
||||
return RoleDelete.Response.fail("Role Delete Fail");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.bpgroup.poc.admin.web.main.admin.role.reqres;
|
||||
|
||||
import com.bpgroup.poc.admin.web.common.CommonResponse;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
public class RoleCreate {
|
||||
|
||||
@Data
|
||||
public static class Request {
|
||||
@NotBlank
|
||||
private String name;
|
||||
private String description;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public static class Response extends CommonResponse {
|
||||
private Long id;
|
||||
|
||||
@Builder
|
||||
public static RoleCreate.Response success(Long id) {
|
||||
RoleCreate.Response response = new RoleCreate.Response();
|
||||
response.resultCode = "0000";
|
||||
response.resultMessage = "Success";
|
||||
response.id = id;
|
||||
return response;
|
||||
}
|
||||
|
||||
@Builder
|
||||
public static RoleCreate.Response fail() {
|
||||
RoleCreate.Response response = new RoleCreate.Response();
|
||||
response.resultCode = "9999";
|
||||
response.resultMessage = "Fail";
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.bpgroup.poc.admin.web.main.admin.role.reqres;
|
||||
|
||||
import com.bpgroup.poc.admin.web.common.CommonResponse;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
public class RoleDelete {
|
||||
@Data
|
||||
public static class Request {
|
||||
@NotNull
|
||||
private Long id;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public static class Response extends CommonResponse {
|
||||
@Builder
|
||||
public static RoleDelete.Response success() {
|
||||
RoleDelete.Response response = new RoleDelete.Response();
|
||||
response.resultCode = "0000";
|
||||
response.resultMessage = "Success";
|
||||
return response;
|
||||
}
|
||||
|
||||
@Builder
|
||||
public static RoleDelete.Response fail(String resultMessage) {
|
||||
RoleDelete.Response response = new RoleDelete.Response();
|
||||
response.resultCode = "9999";
|
||||
response.resultMessage = resultMessage;
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.bpgroup.poc.admin.web.main.admin.role.reqres;
|
||||
|
||||
import com.bpgroup.poc.admin.web.common.CommonResponse;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
public class RoleUpdate {
|
||||
|
||||
@Data
|
||||
public static class Request {
|
||||
@NotNull
|
||||
private Long id;
|
||||
@NotBlank
|
||||
private String name;
|
||||
private String description;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public static class Response extends CommonResponse {
|
||||
@Builder
|
||||
public static RoleUpdate.Response success() {
|
||||
RoleUpdate.Response response = new RoleUpdate.Response();
|
||||
response.resultCode = "0000";
|
||||
response.resultMessage = "Success";
|
||||
return response;
|
||||
}
|
||||
|
||||
@Builder
|
||||
public static RoleUpdate.Response fail(String resultMessage) {
|
||||
RoleUpdate.Response response = new RoleUpdate.Response();
|
||||
response.resultCode = "9999";
|
||||
response.resultMessage = resultMessage;
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
|
||||
@font-face {
|
||||
font-family: 'Pretendard';
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: local('Pretendard Black'), url(../font/Pretendard-Black.woff2) format('woff2'), url(../font/Pretendard-Black.woff) format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Pretendard';
|
||||
font-weight: 800;
|
||||
font-display: swap;
|
||||
src: local('Pretendard ExtraBold'), url(../font/Pretendard-ExtraBold.woff2) format('woff2'), url(../font/Pretendard-ExtraBold.woff) format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Pretendard';
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: local('Pretendard Bold'), url(../font/Pretendard-Bold.woff2) format('woff2'), url(../font/Pretendard-Bold.woff) format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Pretendard';
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: local('Pretendard SemiBold'), url(../font/Pretendard-SemiBold.woff2) format('woff2'), url(../font/Pretendard-SemiBold.woff) format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Pretendard';
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: local('Pretendard Medium'), url(../font/Pretendard-Medium.woff2) format('woff2'), url(../font/Pretendard-Medium.woff) format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Pretendard';
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: local('Pretendard Regular'), url(../font/Pretendard-Regular.woff2) format('woff2'), url(../font/Pretendard-Regular.woff) format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Pretendard';
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: local('Pretendard Light'), url(../font/Pretendard-Light.woff2) format('woff2'), url(../font/Pretendard-Light.woff) format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Pretendard';
|
||||
font-weight: 200;
|
||||
font-display: swap;
|
||||
src: local('Pretendard ExtraLight'), url(../font/Pretendard-ExtraLight.woff2) format('woff2'), url(../font/Pretendard-ExtraLight.woff) format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Pretendard';
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: local('Pretendard Thin'), url(../font/Pretendard-Thin.woff2) format('woff2'), url(../font/Pretendard-Thin.woff) format('woff');
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre, a,
|
||||
abbr, address, big, cite, code, del,
|
||||
abbr, acsronym, address, big, cite, code, del,
|
||||
dfn, em, img, ins, kbd, q, s, samp, small,
|
||||
strike, strong, sub, sup, tt, var, b, u, i,
|
||||
center, dl, dt, dd, ol, ul, li, fieldset, form,
|
||||
|
|
@ -15,12 +15,13 @@ summary, time, mark, audio, video, button, a, input {
|
|||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
box-sizing: border-box;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
-webkit-tap-highlight-color : rgba(0,0,0,0);
|
||||
line-height: 150%;
|
||||
box-sizing: border-box;
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
|
|
@ -33,7 +34,7 @@ body {
|
|||
line-height: 1
|
||||
}
|
||||
ol, ul {
|
||||
list-style: none
|
||||
list-style: none;
|
||||
}
|
||||
blockquote, q {
|
||||
quotes: none
|
||||
|
|
@ -50,7 +51,3 @@ table {
|
|||
a {
|
||||
text-decoration: none
|
||||
}
|
||||
* {
|
||||
font-family: 'Pretendard', 'sans-serif';
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
|
||||
/*20240220 김보라 퍼블수정*/
|
||||
html, body {height:calc(100vh - 72px);}
|
||||
.wrapper {height:100%}
|
||||
#dayKo {margin-left: 20px;}
|
||||
.terminal_wrap {margin:20px 0 10px 0; padding:15px; background:#f5f5f5}
|
||||
.terminal_wrap .terminal_top{overflow: hidden;}
|
||||
.terminal_wrap .terminal_top .terminal_btn {float:right;}
|
||||
.terminal_wrap .terminal_list_wrap {border-top:1px solid #ddd; margin: 10px 0 0 0; padding:10px}
|
||||
.terminal_list_wrap .terminal_list_in {padding: 5px 0;}
|
||||
.terminal_list_wrap .terminal_list_in .checkbox-label {line-height: normal; padding-left: 25px; display: inline;}
|
||||
.terminal_list_wrap .terminal_list_in > p {text-align: center;}
|
||||
|
||||
.table_radio_wrap {vertical-align: middle;}
|
||||
.table_radio_wrap .radio_box {display: inline-block !important; padding-right:10px !important; padding-left: 0; margin-left:0; }
|
||||
.table_radio_wrap .radio_box input[type="radio"] + label {position:static;}
|
||||
|
||||
.btn_wrap {position: relative; text-align: right; margin-top:10px;}
|
||||
.table_select_box, .table_date_box {width: 225px;}
|
||||
|
||||
.tb_sty01.tb_sty02 {border-top:1px solid #999}
|
||||
|
||||
#batchRegisterTerminalModal .modal-content {max-width: 530px !important}
|
||||
#depositDetailModal .modal-content, #withdrawDetailModal .modal-content,
|
||||
#transferDetailModal .modal-content, #transferHistoryModal .modal-content {max-width: 750px !important}
|
||||
|
||||
.btn_normal.excelbtn {
|
||||
height: 36px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
line-height: 36px;
|
||||
}
|
||||
.tb_wrapper.tb_wrapper_x_scroll {overflow-x: auto;}
|
||||
.tb_wrapper.tb_wrapper_terminal {max-height: 170px; overflow-y: auto; overflow-x: auto;}
|
||||
.wd_auto .tbl_bert_m > th{vertical-align: middle;}
|
||||
.wd_auto input {width:100% !important;}
|
||||
|
||||
.font_bold{font-weight: bold;}
|
||||
.tooltip_prt {position:relative;}
|
||||
.tooltip_chd {
|
||||
visibility: hidden;
|
||||
background-color: black;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
padding: 5px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
||||
width: max-content;
|
||||
bottom: -5%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 50%);
|
||||
}
|
||||
.tooltip_prt:hover .tooltip_chd {
|
||||
visibility: visible;
|
||||
}
|
||||
table colgroup > col { min-width:max-content !important}
|
||||
|
||||
table td > p, table td > a {
|
||||
display:block;
|
||||
overflow:hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.mt-05 {
|
||||
margin-top: 5px;
|
||||
}
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.tb_wrapper {padding-bottom:35px}
|
||||
/*20240226 김현종 퍼블 수정*/
|
||||
.clickable_text a:link, .clickable_text a:visited {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*20240229 김현종 퍼블 수정*/
|
||||
.title_text a {
|
||||
color: black; /* 원하는 색상으로 설정 */
|
||||
text-decoration: none; /* 밑줄 제거 */
|
||||
}
|
||||
|
||||
.title_text a:visited {
|
||||
color: black; /* 방문한 링크에 대해서도 동일한 색상 유지 */
|
||||
}
|
||||
|
||||
|
||||
/* 모바일 */
|
||||
@media (max-width: 767px){
|
||||
.mb_w50 {overflow: hidden; margin-left:-5px;}
|
||||
.mb_w50 > span {width:50%; float:left; padding:0 3px !important; box-sizing: border-box;}
|
||||
.mb_w50 > span select {width:100%}
|
||||
|
||||
table td > p, table td > a {
|
||||
overflow: inherit;
|
||||
white-space: inherit;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.tb_w_auto {width:130px !important;}
|
||||
.table_wrap h4 { height: auto !important}
|
||||
.table_wrap2 .tb_right {position:absolute;}
|
||||
}
|
||||
|
||||
/* 테블릿 세로 */
|
||||
@media (min-width: 768px) and (max-width: 991px) {
|
||||
.menu_wrap {
|
||||
max-width:200px
|
||||
}
|
||||
|
||||
.tb_w_auto {width:100px}
|
||||
}
|
||||
|
||||
|
||||
/* 테블릿 가로 */
|
||||
|
||||
@media (min-width: 992px) and (max-width: 1199px) {
|
||||
.menu_wrap {
|
||||
max-width:200px
|
||||
}
|
||||
|
||||
.tb_w_auto {width:130px}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* 데스크탑 일반 */
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
|
||||
}
|
||||