feature/admin #6
|
|
@ -54,6 +54,16 @@ dependencies {
|
||||||
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
|
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
|
||||||
runtimeOnly 'io.jsonwebtoken:jjwt-jackson: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 com.bpgroup.poc.admin.domain.admin.entity.Administrator;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
|
||||||
|
|
@ -21,6 +22,7 @@ public class AdministratorCreateCommand {
|
||||||
@NotBlank
|
@NotBlank
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
@Builder
|
||||||
public static AdministratorCreateCommand of(String loginId, String password, String email, String name) {
|
public static AdministratorCreateCommand of(String loginId, String password, String email, String name) {
|
||||||
AdministratorCreateCommand command = new AdministratorCreateCommand();
|
AdministratorCreateCommand command = new AdministratorCreateCommand();
|
||||||
command.loginId = loginId;
|
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 com.bpgroup.poc.admin.domain.admin.entity.Administrator;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
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 com.bpgroup.poc.admin.domain.BaseEntity;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
|
@ -25,4 +26,17 @@ public class Role extends BaseEntity {
|
||||||
@OneToMany(mappedBy = "role", fetch = FetchType.LAZY)
|
@OneToMany(mappedBy = "role", fetch = FetchType.LAZY)
|
||||||
private Set<RoleMenu> roleMenus = new HashSet<>();
|
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 class DuplicationAdministratorException extends RuntimeException {
|
||||||
public DuplicationAdministratorException(String loginId) {
|
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;
|
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;
|
package com.bpgroup.poc.admin.security;
|
||||||
|
|
||||||
import com.bpgroup.poc.admin.common.FormatHelper;
|
import com.bpgroup.poc.admin.security.authentication.CustomAuthenticationFilter;
|
||||||
import com.bpgroup.poc.admin.security.authentication.AuthenticationFailException;
|
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 lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
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.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.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 org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
|
@ -20,28 +31,32 @@ public class SecurityConfig {
|
||||||
private static final String LOGOUT_PATH = "/logout";
|
private static final String LOGOUT_PATH = "/logout";
|
||||||
private static final String ERROR_PATH = "/error";
|
private static final String ERROR_PATH = "/error";
|
||||||
|
|
||||||
|
private final LoginService loginService;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
|
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
|
http.authorizeHttpRequests(c -> c
|
||||||
.requestMatchers("/css/**", "/images/**", "/js/**").permitAll()
|
.requestMatchers("/css/**", "/images/**", "/js/**", "/font/**").permitAll()
|
||||||
.requestMatchers("/common/modal/**").permitAll()
|
.requestMatchers("/common/modal/**").permitAll()
|
||||||
.requestMatchers(LOGIN_PATH, LOGOUT_PATH, ERROR_PATH).permitAll()
|
.requestMatchers(LOGIN_PATH, LOGOUT_PATH, ERROR_PATH).permitAll()
|
||||||
.anyRequest().authenticated());
|
.anyRequest().authenticated());
|
||||||
|
|
||||||
http.formLogin(c -> c
|
http.formLogin(AbstractHttpConfigurer::disable); // Form 로그인이 아닌 Json 로그인으로 분리
|
||||||
.loginPage(LOGIN_PATH)
|
http.addFilterBefore(authenticationGenerateFilter(), UsernamePasswordAuthenticationFilter.class); // 로그인 관련 Filter 설정
|
||||||
.loginProcessingUrl(LOGIN_PATH)
|
|
||||||
.usernameParameter("loginId")
|
http.addFilterAfter(new JwtTokenValidatorFilter(), BasicAuthenticationFilter.class);
|
||||||
.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.logout(c -> c
|
http.logout(c -> c
|
||||||
.logoutRequestMatcher(new AntPathRequestMatcher(LOGOUT_PATH, "GET"))
|
.logoutRequestMatcher(new AntPathRequestMatcher(LOGOUT_PATH, "GET"))
|
||||||
|
|
@ -51,12 +66,23 @@ public class SecurityConfig {
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Bcrypt Version, Bcrypt Strength, Salt String 설정은 생성자를 이용하여 설정 가능
|
|
||||||
*/
|
|
||||||
@Bean
|
@Bean
|
||||||
public BCryptPasswordEncoder passwordEncoder() {
|
public CustomAuthenticationProvider customAuthenticationProvider() {
|
||||||
return new BCryptPasswordEncoder();
|
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;
|
package com.bpgroup.poc.admin.security.authentication;
|
||||||
|
|
||||||
import com.bpgroup.poc.admin.app.login.LoginResult;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import lombok.EqualsAndHashCode;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.ToString;
|
import lombok.NoArgsConstructor;
|
||||||
import org.springframework.util.LinkedMultiValueMap;
|
|
||||||
import org.springframework.util.MultiValueMap;
|
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
public class AuthenticationDetail {
|
public class AuthenticationDetail {
|
||||||
|
|
||||||
private Long id;
|
|
||||||
private String loginId;
|
private String loginId;
|
||||||
private String name;
|
private String name;
|
||||||
private String email;
|
private String email;
|
||||||
|
|
||||||
private MultiValueMap<MenuOneDepth, MenuTwoDepth> menus = new LinkedMultiValueMap<>();
|
@Builder
|
||||||
|
public static AuthenticationDetail of(String loginId, String name, String email) {
|
||||||
public static AuthenticationDetail from(LoginResult result) {
|
|
||||||
AuthenticationDetail authenticationDetail = new AuthenticationDetail();
|
AuthenticationDetail authenticationDetail = new AuthenticationDetail();
|
||||||
authenticationDetail.id = result.getId();
|
authenticationDetail.loginId = loginId;
|
||||||
authenticationDetail.loginId = result.getLoginId();
|
authenticationDetail.name = name;
|
||||||
authenticationDetail.name = result.getName();
|
authenticationDetail.email = email;
|
||||||
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()
|
|
||||||
)
|
|
||||||
));
|
|
||||||
|
|
||||||
return authenticationDetail;
|
return authenticationDetail;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public String toJsonString() {
|
||||||
* EqualsAndHashCode annotattion이 없을 경우 MultiValueMap을 사용할 때 중복된 key를 찾지 못함
|
try {
|
||||||
*/
|
return new ObjectMapper().writeValueAsString(this);
|
||||||
@Getter
|
} catch (JsonProcessingException e) {
|
||||||
@ToString
|
throw new RuntimeException(e);
|
||||||
@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 static AuthenticationDetail fromJsonString(String jsonString) {
|
||||||
|
try {
|
||||||
@Getter
|
return new ObjectMapper().readValue(jsonString, AuthenticationDetail.class);
|
||||||
@ToString
|
} catch (JsonProcessingException e) {
|
||||||
private static class MenuTwoDepth {
|
throw new RuntimeException(e);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,26 @@
|
||||||
package com.bpgroup.poc.admin.security.authentication;
|
package com.bpgroup.poc.admin.security.authentication;
|
||||||
|
|
||||||
import com.bpgroup.poc.admin.app.login.exception.AdministratorNotFoundException;
|
import com.bpgroup.poc.admin.security.authentication.service.exception.AdministratorNotFoundException;
|
||||||
import com.bpgroup.poc.admin.app.login.exception.DoNotHaveAnyMenuException;
|
import com.bpgroup.poc.admin.security.authentication.service.exception.DoNotHaveAnyMenuException;
|
||||||
import com.bpgroup.poc.admin.app.login.exception.InvalidPasswordException;
|
import com.bpgroup.poc.admin.security.authentication.service.exception.InvalidPasswordException;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
@Slf4j
|
@Getter
|
||||||
|
@RequiredArgsConstructor
|
||||||
public enum AuthenticationFailReason {
|
public enum AuthenticationFailReason {
|
||||||
WRONG_LOGIN_ID,
|
WRONG_LOGIN_ID("아이디 및 패스워드를 확인하세요."),
|
||||||
WRONG_PASSWORD,
|
WRONG_PASSWORD("아이디 및 패스워드를 확인하세요."),
|
||||||
HAVE_NO_MENU,
|
HAVE_NO_MENU("등록된 메뉴 권한이 없습니다. \n메뉴 등록 후 사용하시기 바랍니다."),
|
||||||
INTERNAL_ERROR;
|
INTERNAL_ERROR("서버 내부 오류가 발생했습니다.");
|
||||||
|
|
||||||
|
private final String message;
|
||||||
|
|
||||||
public static AuthenticationFailReason from(Exception e) {
|
public static AuthenticationFailReason from(Exception e) {
|
||||||
if (e instanceof AdministratorNotFoundException || e instanceof InvalidPasswordException) {
|
if (e instanceof AdministratorNotFoundException) {
|
||||||
return WRONG_LOGIN_ID;
|
return WRONG_LOGIN_ID;
|
||||||
|
} else if (e instanceof InvalidPasswordException) {
|
||||||
|
return WRONG_PASSWORD;
|
||||||
} else if (e instanceof DoNotHaveAnyMenuException) {
|
} else if (e instanceof DoNotHaveAnyMenuException) {
|
||||||
return HAVE_NO_MENU;
|
return HAVE_NO_MENU;
|
||||||
} else {
|
} 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;
|
package com.bpgroup.poc.admin.security.authentication;
|
||||||
|
|
||||||
import com.bpgroup.poc.admin.app.login.LoginResult;
|
import com.bpgroup.poc.admin.security.authentication.service.LoginResult;
|
||||||
import com.bpgroup.poc.admin.app.login.LoginService;
|
import com.bpgroup.poc.admin.security.authentication.service.LoginService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.security.authentication.AuthenticationProvider;
|
import org.springframework.security.authentication.AuthenticationProvider;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class CustomAuthenticationProvider implements AuthenticationProvider {
|
public class CustomAuthenticationProvider implements AuthenticationProvider {
|
||||||
|
|
||||||
private final LoginService loginService;
|
private final LoginService loginService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||||
String loginId = (String) authentication.getPrincipal();
|
try {
|
||||||
|
String username = (String) authentication.getPrincipal();
|
||||||
String password = (String) authentication.getCredentials();
|
String password = (String) authentication.getCredentials();
|
||||||
|
|
||||||
try {
|
LoginResult loginResult = loginService.login(username, password);
|
||||||
LoginResult loginResult = loginService.login(loginId, password);
|
|
||||||
return buildAuthenticationToken(loginResult);
|
return buildAuthenticationToken(loginResult);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new AuthenticationFailException("로그인에 실패하였습니다.", AuthenticationFailReason.from(e));
|
throw new AuthenticationFailException("로그인에 실패하였습니다.", AuthenticationFailReason.from(e));
|
||||||
|
|
@ -29,13 +30,22 @@ public class CustomAuthenticationProvider implements AuthenticationProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
private UsernamePasswordAuthenticationToken buildAuthenticationToken(LoginResult result) {
|
private UsernamePasswordAuthenticationToken buildAuthenticationToken(LoginResult result) {
|
||||||
UsernamePasswordAuthenticationToken authenticationToken = UsernamePasswordAuthenticationToken.authenticated(
|
UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated(
|
||||||
result.getLoginId(),
|
result.getLoginId(),
|
||||||
null,
|
null,
|
||||||
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
|
@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 class AdministratorNotFoundException extends Exception {
|
||||||
public AdministratorNotFoundException(String message) {
|
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 class DoNotHaveAnyMenuException extends Exception {
|
||||||
public DoNotHaveAnyMenuException() {
|
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 class InvalidPasswordException extends Exception {
|
||||||
public InvalidPasswordException(String message) {
|
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 jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
package com.bpgroup.poc.admin.web.common;
|
package com.bpgroup.poc.admin.web.common;
|
||||||
|
|
||||||
public class CommonResponse {
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class CommonResponse {
|
||||||
protected String resultCode;
|
protected String resultCode;
|
||||||
protected String resultMessage;
|
protected String resultMessage;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,16 @@
|
||||||
package com.bpgroup.poc.admin.web.login;
|
package com.bpgroup.poc.admin.web.login;
|
||||||
|
|
||||||
import com.bpgroup.poc.admin.security.authentication.AuthenticationFailReason;
|
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/login")
|
@RequestMapping("/login")
|
||||||
public class LoginController {
|
public class LoginController {
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public String loginPage(
|
public String loginPage() {
|
||||||
@RequestParam(required = false) AuthenticationFailReason error,
|
|
||||||
Model model
|
|
||||||
) {
|
|
||||||
if (error != null) {
|
|
||||||
model.addAttribute("errorMessage", getMessage(error));
|
|
||||||
}
|
|
||||||
|
|
||||||
return "login/login";
|
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 jakarta.validation.constraints.NotBlank;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.validation.BindingResult;
|
import org.springframework.validation.BindingResult;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
@ -18,7 +19,8 @@ import java.util.List;
|
||||||
@RequestMapping("/admin/management")
|
@RequestMapping("/admin/management")
|
||||||
public class AdministratorManagementRestController {
|
public class AdministratorManagementRestController {
|
||||||
|
|
||||||
private final AdministratorManagementWebService administratorManagementWebService;
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final AdministratorManagementService administratorManagementService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 전체 조회
|
* 전체 조회
|
||||||
|
|
@ -26,7 +28,7 @@ public class AdministratorManagementRestController {
|
||||||
*/
|
*/
|
||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
public ResponseEntity<?> getAdministrators() {
|
public ResponseEntity<?> getAdministrators() {
|
||||||
List<AdministratorFind.Response> response = administratorManagementWebService.findAll();
|
List<AdministratorFind.Response> response = administratorManagementService.findAll();
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,7 +39,7 @@ public class AdministratorManagementRestController {
|
||||||
*/
|
*/
|
||||||
@GetMapping("/{loginId}")
|
@GetMapping("/{loginId}")
|
||||||
public ResponseEntity<?> getAdministrator(@PathVariable @NotBlank String loginId) {
|
public ResponseEntity<?> getAdministrator(@PathVariable @NotBlank String loginId) {
|
||||||
AdministratorFind.Response response = administratorManagementWebService.find(loginId);
|
AdministratorFind.Response response = administratorManagementService.find(loginId);
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,7 +54,8 @@ public class AdministratorManagementRestController {
|
||||||
@RequestBody @Validated AdministratorCreate.Request request,
|
@RequestBody @Validated AdministratorCreate.Request request,
|
||||||
BindingResult bindingResult
|
BindingResult bindingResult
|
||||||
) {
|
) {
|
||||||
AdministratorCreate.Response response = administratorManagementWebService.create(request);
|
request.setPassword(passwordEncoder.encode(request.getPassword()));
|
||||||
|
AdministratorCreate.Response response = administratorManagementService.create(request);
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,8 +64,9 @@ public class AdministratorManagementRestController {
|
||||||
@RequestBody @Validated AdministratorUpdate.Request request,
|
@RequestBody @Validated AdministratorUpdate.Request request,
|
||||||
BindingResult bindingResult
|
BindingResult bindingResult
|
||||||
) {
|
) {
|
||||||
administratorManagementWebService.update(request);
|
request.setPassword(passwordEncoder.encode(request.getPassword()));
|
||||||
return ResponseEntity.ok().build();
|
AdministratorUpdate.Response response = administratorManagementService.update(request);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -76,8 +80,8 @@ public class AdministratorManagementRestController {
|
||||||
@RequestBody @Validated AdministratorDelete.Request request,
|
@RequestBody @Validated AdministratorDelete.Request request,
|
||||||
BindingResult bindingResult
|
BindingResult bindingResult
|
||||||
) {
|
) {
|
||||||
administratorManagementWebService.delete(request);
|
AdministratorDelete.Response response = administratorManagementService.delete(request);
|
||||||
return ResponseEntity.ok().build();
|
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;
|
package com.bpgroup.poc.admin.web.main.admin.management.reqres;
|
||||||
|
|
||||||
|
import com.bpgroup.poc.admin.web.common.CommonResponse;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
public class AdministratorDelete {
|
public class AdministratorDelete {
|
||||||
|
|
||||||
|
|
@ -11,4 +15,24 @@ public class AdministratorDelete {
|
||||||
private Long id;
|
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;
|
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.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
public class AdministratorUpdate {
|
public class AdministratorUpdate {
|
||||||
|
|
||||||
|
|
@ -24,4 +28,23 @@ public class AdministratorUpdate {
|
||||||
private String name;
|
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,
|
html, body, div, span, applet, object, iframe,
|
||||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre, a,
|
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,
|
dfn, em, img, ins, kbd, q, s, samp, small,
|
||||||
strike, strong, sub, sup, tt, var, b, u, i,
|
strike, strong, sub, sup, tt, var, b, u, i,
|
||||||
center, dl, dt, dd, ol, ul, li, fieldset, form,
|
center, dl, dt, dd, ol, ul, li, fieldset, form,
|
||||||
|
|
@ -15,12 +15,13 @@ summary, time, mark, audio, video, button, a, input {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
font-size: 100%;
|
font-size: 100%;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
-webkit-tap-highlight-color : rgba(0,0,0,0);
|
-webkit-tap-highlight-color : rgba(0,0,0,0);
|
||||||
line-height: 150%;
|
line-height: 150%;
|
||||||
box-sizing: border-box;
|
word-break: keep-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* HTML5 display-role reset for older browsers */
|
/* HTML5 display-role reset for older browsers */
|
||||||
|
|
@ -33,7 +34,7 @@ body {
|
||||||
line-height: 1
|
line-height: 1
|
||||||
}
|
}
|
||||||
ol, ul {
|
ol, ul {
|
||||||
list-style: none
|
list-style: none;
|
||||||
}
|
}
|
||||||
blockquote, q {
|
blockquote, q {
|
||||||
quotes: none
|
quotes: none
|
||||||
|
|
@ -50,7 +51,3 @@ table {
|
||||||
a {
|
a {
|
||||||
text-decoration: none
|
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) {
|
|
||||||
|
|
||||||
}
|
|
||||||