feature/admin #6

Merged
gh.yeom merged 8 commits from feature/admin into main 2024-05-14 16:51:29 +09:00
132 changed files with 4239 additions and 1397 deletions

View File

@ -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'
} }

View File

@ -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');
}

View File

@ -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
}

View File

@ -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);
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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="#">&laquo;</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="#">&raquo;</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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

File diff suppressed because one or more lines are too long

View File

@ -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";
}
}
});

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}
} }

View File

@ -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) {

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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;
} }
} }

View File

@ -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();
}
}

View File

@ -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"};
}

View File

@ -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;
} }
} }
@Getter public static AuthenticationDetail fromJsonString(String jsonString) {
@ToString try {
private static class MenuTwoDepth { return new ObjectMapper().readValue(jsonString, AuthenticationDetail.class);
private String uri; } catch (JsonProcessingException e) {
private String name; throw new RuntimeException(e);
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;
} }
} }

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

@ -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();
String password = (String) authentication.getCredentials();
try { try {
LoginResult loginResult = loginService.login(loginId, password); String username = (String) authentication.getPrincipal();
String password = (String) authentication.getCredentials();
LoginResult loginResult = loginService.login(username, password);
return buildAuthenticationToken(loginResult); 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

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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()
);
}
}

View File

@ -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) {

View File

@ -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() {

View File

@ -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) {

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -1,4 +0,0 @@
package com.bpgroup.poc.admin.values;
public class AdministratorMenu {
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
} }

View File

@ -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 -> "서버에 오류가 발생했습니다.";
};
}
} }

View File

@ -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);
} }
} }

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}
} }

View File

@ -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;
}
}
} }

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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");
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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');
}

View File

@ -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;
}

File diff suppressed because it is too large Load Diff

View File

@ -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) {
}

Some files were not shown because too many files have changed in this diff Show More