feat: 로그인과 회원가입 퍼블리싱

This commit is contained in:
hong
2026-07-28 18:09:20 +09:00
parent 80740a9543
commit 1f51c9b21e
20 changed files with 1871 additions and 1327 deletions
@@ -74,7 +74,7 @@ public class AppServiceFacade {
Arrays.asList(new ProcessingState(), new RequestedState()));
// 승인정보(approval) 없는 신청도 목록에 노출한다. (사용자가 직접 삭제 가능)
appRequests.addAll(appRequestRepository.findAllByOrgAndTypeIsInAndApprovalIsNull(portalOrg, types));
appRequests.addAll(appRequestRepository .findAllByOrgAndTypeIsInAndApprovalIsNull(portalOrg, types));
return appRequests;
}
+388 -183
View File
@@ -2927,6 +2927,13 @@ hr {
transform: translateY(-2px);
color: #fff;
}
.btn-action-primary:disabled {
background: #cbd5e1 !important;
color: #94a3b8 !important;
cursor: not-allowed !important;
transform: none !important;
pointer-events: none !important;
}
.btn-action-primary.md {
font-size: 14px;
padding: 11px 45px;
@@ -5286,6 +5293,69 @@ select.form-control {
transform: translateY(0);
}
}
.password-policy-checklist {
list-style: none;
padding: 0;
margin: 12px 0 0 0;
}
.password-policy-checklist li {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
line-height: 20px;
margin-bottom: 6px;
transition: color 0.15s ease;
}
.password-policy-checklist li:last-child {
margin-bottom: 0;
}
.password-policy-checklist li .policy-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
flex-shrink: 0;
font-size: 13px;
font-weight: 700;
line-height: 1;
}
.password-policy-checklist li .policy-icon::before {
content: "•";
}
.password-policy-checklist li.is-idle {
color: #718096;
}
.password-policy-checklist li.is-idle .policy-icon {
color: #a0aec0;
}
.password-policy-checklist li.is-pass {
color: #1a8f4c;
}
.password-policy-checklist li.is-pass .policy-icon {
color: #1a8f4c;
}
.password-policy-checklist li.is-pass .policy-icon::before {
content: "✔";
}
.password-policy-checklist li.is-fail {
color: #d63a3a;
}
.password-policy-checklist li.is-fail .policy-icon {
color: #d63a3a;
}
.password-policy-checklist li.is-fail .policy-icon::before {
content: "✖";
}
.password-policy-note {
margin: 10px 0 0 0;
font-size: 14px;
line-height: 18px;
color: #ef5555;
}
.auth-group {
display: flex;
align-items: center;
@@ -12859,16 +12929,14 @@ body.index-page-body {
}
}
.signup-selection-page {
min-height: calc(100vh - 380px);
display: flex;
align-items: center;
justify-content: center;
background: #EDF9FE;
padding: 30px 20px;
padding: 100px 20px;
position: relative;
border-radius: 12px;
margin-top: 30px;
margin-bottom: 30px;
margin-top: 60px;
margin-bottom: 50px;
}
.signup-selection-container {
@@ -12891,104 +12959,195 @@ body.index-page-body {
}
}
.signup-logo {
width: 90px;
height: 90px;
margin-bottom: 15px;
}
.signup-logo img {
.signup-header {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 40px;
width: 100%;
height: 100%;
object-fit: contain;
}
.signup-title-row {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin-bottom: 12px;
color: #000000;
}
.signup-title-row .signup-title-icon {
color: #000000;
flex-shrink: 0;
}
.signup-title-row .signup-title {
font-family: "Spoqa Han Sans Neo", "SpoqaHanSans", "Noto Sans KR", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 36px;
font-weight: 700;
color: #000000;
margin: 0;
letter-spacing: -0.5px;
}
.signup-message {
font-family: "Spoqa Han Sans Neo", "SpoqaHanSans", "Noto Sans KR", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 20px;
font-size: 16px;
font-weight: 400;
color: #000000;
color: #718096;
text-align: center;
margin-bottom: 20px;
line-height: 1;
margin: 0;
line-height: 1.5;
}
.signup-buttons {
.signup-cards {
width: 100%;
max-width: 504px;
max-width: 760px;
display: flex;
flex-direction: row;
gap: 28px;
margin-bottom: 45px;
justify-content: center;
}
.signup-card-item {
flex: 1;
background: #FFFFFF;
border: 1px solid rgba(226, 232, 240, 0.8);
border-radius: 24px;
padding: 45px 30px;
display: flex;
flex-direction: column;
gap: 20px;
margin-bottom: 30px;
}
.signup-btn {
width: 100%;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 10px;
font-family: "Spoqa Han Sans Neo", "SpoqaHanSans", "Noto Sans KR", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 20px;
font-weight: 700;
color: #FFFFFF;
text-align: center;
text-decoration: none;
border-radius: 12px;
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
color: #1A202C;
cursor: pointer;
transition: all 0.3s ease;
line-height: 1;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.03);
position: relative;
overflow: hidden;
}
.signup-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
.signup-card-item::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at top right, rgba(255, 255, 255, 0.8), transparent 70%);
opacity: 0;
transition: opacity 0.4s ease;
pointer-events: none;
}
.signup-btn:active {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
.signup-btn .signup-btn-icon {
width: 26px;
height: 30px;
flex-shrink: 0;
}
.signup-btn span {
white-space: nowrap;
}
.signup-btn-individual {
background: #0049B4;
}
.signup-btn-organization {
background: #00A1D7;
}
.signup-navigation {
.signup-card-item .card-icon-wrapper {
width: 76px;
height: 76px;
border-radius: 22px;
display: flex;
justify-content: center;
align-items: center;
gap: 26px;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 24px;
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.02);
}
.signup-navigation .signup-nav-link {
.signup-card-item .card-text-wrapper {
display: flex;
flex-direction: column;
align-items: center;
}
.signup-card-item .card-title {
font-family: "Spoqa Han Sans Neo", "SpoqaHanSans", "Noto Sans KR", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 16px;
font-weight: 400;
color: #000000;
text-decoration: none;
transition: color 0.3s ease;
line-height: 1;
font-size: 22px;
font-weight: 800;
margin-top: 0;
margin-bottom: 12px;
line-height: 1.3;
color: #1A202C;
transition: all 0.4s ease;
}
.signup-navigation .signup-nav-link:hover {
.signup-card-item .card-desc {
font-family: "Spoqa Han Sans Neo", "SpoqaHanSans", "Noto Sans KR", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 14px;
color: #718096;
line-height: 1.6;
margin: 0;
max-width: 250px;
word-break: keep-all;
transition: color 0.4s ease;
}
.signup-card-item:hover {
transform: translateY(-8px);
background: #FFFFFF;
}
.signup-card-item:hover::before {
opacity: 1;
}
.signup-card-item.individual-card .card-icon-wrapper {
background: linear-gradient(135deg, #F0F5FF 0%, #D8E5FF 100%);
color: #0049B4;
text-decoration: underline;
}
.signup-navigation .signup-nav-separator {
color: #000000;
font-size: 16px;
line-height: 1;
.signup-card-item.individual-card:hover {
border-color: #0049B4;
box-shadow: 0 20px 40px rgba(0, 73, 180, 0.12);
}
.signup-card-item.individual-card:hover .card-title {
color: #0049B4;
}
.signup-card-item.individual-card:hover .card-icon-wrapper {
background: linear-gradient(135deg, #0049B4 0%, #0066FF 100%);
color: #FFFFFF;
transform: scale(1.05) rotate(3deg);
box-shadow: 0 10px 20px rgba(0, 73, 180, 0.25);
}
.signup-card-item.organization-card .card-icon-wrapper {
background: linear-gradient(135deg, #EBF8FF 0%, #CAF0F8 100%);
color: #00A1D7;
}
.signup-card-item.organization-card:hover {
border-color: #00A1D7;
box-shadow: 0 20px 40px rgba(0, 161, 215, 0.12);
}
.signup-card-item.organization-card:hover .card-title {
color: #00A1D7;
}
.signup-card-item.organization-card:hover .card-icon-wrapper {
background: linear-gradient(135deg, #00A1D7 0%, #33C2FF 100%);
color: #FFFFFF;
transform: scale(1.05) rotate(-3deg);
box-shadow: 0 10px 20px rgba(0, 161, 215, 0.25);
}
.signup-navigation-box {
display: flex;
align-items: center;
background: #EDF4FF;
border: 1px solid #0049B4;
border-radius: 8px;
overflow: hidden;
height: 42px;
max-width: 280px;
width: 100%;
margin: 0 auto;
}
.signup-navigation-box .signup-nav-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font-family: "Spoqa Han Sans Neo", "SpoqaHanSans", "Noto Sans KR", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 14px;
font-weight: 700;
color: #0049B4;
text-decoration: none;
height: 100%;
transition: all 0.2s ease;
}
.signup-navigation-box .signup-nav-btn:hover {
background: #DCE7FF;
}
.signup-navigation-box .signup-nav-divider {
width: 1px;
height: 100%;
background: #0049B4;
}
@media (max-width: 768px) {
@@ -13004,59 +13163,70 @@ body.index-page-body {
max-width: 100%;
padding: 0;
}
.signup-selection-card {
padding: 0;
.signup-header {
margin-bottom: 30px;
}
.signup-logo {
width: 64px;
height: 64px;
margin-bottom: 24px;
.signup-header .signup-title-row {
margin-bottom: 8px;
}
.signup-header .signup-title-row .signup-title-icon {
width: 24px;
height: 24px;
}
.signup-header .signup-title-row .signup-title {
font-size: 24px;
}
.signup-message {
font-size: 14px;
font-weight: 400;
color: #212529;
margin-bottom: 80px;
line-height: 26px;
margin-bottom: 40px;
line-height: 22px;
}
.signup-buttons {
.signup-cards {
max-width: 335px;
gap: 24px;
margin-bottom: 80px;
flex-direction: column;
gap: 16px;
margin-bottom: 40px;
}
.signup-btn {
.signup-card-item {
padding: 24px 20px;
border-radius: 12px;
flex-direction: row;
text-align: left;
align-items: center;
gap: 16px;
}
.signup-card-item .card-icon-wrapper {
width: 44px;
height: 44px;
font-size: 16px;
font-weight: 700;
border-radius: 8px;
padding: 8px 12px;
justify-content: center;
gap: 8px;
}
.signup-btn svg {
width: 14px;
height: 16px;
margin-bottom: 0;
flex-shrink: 0;
}
.signup-btn span {
text-align: center;
.signup-card-item .card-icon-wrapper svg {
width: 20px;
height: 20px;
}
.signup-btn:hover {
.signup-card-item .card-text-wrapper {
align-items: flex-start;
}
.signup-card-item .card-title {
font-size: 16px;
margin-bottom: 4px;
}
.signup-card-item .card-desc {
font-size: 12px;
}
.signup-card-item:hover {
transform: none;
box-shadow: none;
opacity: 0.9;
}
.signup-navigation {
gap: 12px;
.signup-navigation-box {
max-width: 260px;
height: 38px;
}
.signup-navigation .signup-nav-link {
font-size: 14px;
font-weight: 400;
color: #515961;
}
.signup-navigation .signup-nav-separator {
font-size: 14px;
color: #515961;
.signup-navigation-box .signup-nav-btn {
font-size: 13px;
}
}
.corporate-transfer-section {
@@ -21078,7 +21248,7 @@ input[type=checkbox]:checked + .custom-checkbox {
background: none;
padding: 0 0 16px 0;
border-radius: 0;
border-bottom: 2px solid #212529;
border-bottom: 1px solid #212529;
margin-bottom: 32px;
}
.org-section-header--agreement h3 {
@@ -21106,76 +21276,69 @@ input[type=checkbox]:checked + .custom-checkbox {
}
.org-info-notice {
background: #FFF5F5;
border: 1px solid #FED7D7;
border-radius: 8px;
margin-bottom: 16px;
padding: 14px 20px;
display: flex;
align-items: center;
gap: 12px;
margin-top: 16px;
margin-bottom: 32px;
}
.org-info-notice ul {
list-style: none;
padding: 0;
margin: 0;
.org-info-notice .notice-icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
color: #E53E3E;
flex-shrink: 0;
}
.org-info-notice ul li {
position: relative;
padding-left: 24px;
color: #64748B;
.org-info-notice .notice-text {
font-family: "Spoqa Han Sans Neo", "SpoqaHanSans", "Noto Sans KR", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 14px;
line-height: 1.6;
}
.org-info-notice ul li::before {
content: "•";
position: absolute;
left: 0;
color: #0049b4;
font-weight: 700;
}
.org-info-notice ul li + li {
margin-top: 8px;
font-weight: 500;
color: #C53030;
line-height: 1.5;
margin: 0;
}
.org-form-group {
margin-bottom: 16px;
margin-bottom: 26px;
display: flex;
align-items: flex-start;
gap: 20px;
flex-direction: column;
align-items: stretch;
gap: 13px;
}
.org-form-group:last-child {
margin-bottom: 0;
}
@media (max-width: 768px) {
.org-form-group {
flex-direction: column;
gap: 10px;
margin-bottom: 12px;
}
}
.org-form-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 400;
color: #1A1A2E;
min-width: 160px;
padding-top: 10px;
font-family: "Spoqa Han Sans Neo", "SpoqaHanSans", "Noto Sans KR", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 16px;
font-weight: 700;
color: #1A202C;
width: auto;
margin: 0;
padding: 0;
}
.org-form-label .required-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 2px 8px;
background: #3BA4ED;
color: #FFFFFF;
font-size: 12px;
font-weight: 500;
border-radius: 4px;
line-height: 1.2;
visibility: hidden;
position: relative;
width: 10px;
}
@media (max-width: 768px) {
.org-form-label {
min-width: auto;
padding-top: 0;
}
.org-form-label .required-badge::after {
content: "*";
visibility: visible;
position: absolute;
left: 4px;
top: 0;
color: #f4253c;
font-size: 16px;
font-weight: 700;
}
.org-form-input-wrapper {
@@ -21194,6 +21357,8 @@ input[type=checkbox]:checked + .custom-checkbox {
display: flex;
align-items: center;
gap: 16px;
width: 100%;
max-width: 650px;
}
.org-input-row .org-form-input,
.org-input-row .org-compound-input,
@@ -21222,18 +21387,20 @@ input[type=checkbox]:checked + .custom-checkbox {
.org-form-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #E2E8F0;
padding: 10px 16px;
border: 1px solid #CBD5E0;
border-radius: 8px;
font-family: "Spoqa Han Sans Neo", "SpoqaHanSans", "Noto Sans KR", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 14px;
color: #1A1A2E;
color: #1A202C;
transition: all 0.3s ease;
background: #FFFFFF;
background: #FCFDFD;
}
.org-form-input:focus {
outline: none;
border-color: #0049b4;
box-shadow: 0 0 0 3px rgba(75, 155, 255, 0.1);
border-color: #0049B4;
box-shadow: 0 0 0 3px rgba(0, 73, 180, 0.15);
background: #FFFFFF;
}
.org-form-input:disabled {
background: #F8FAFC;
@@ -21247,7 +21414,7 @@ input[type=checkbox]:checked + .custom-checkbox {
border-color: #6BCF7F;
}
.org-form-input::placeholder {
color: #94A3B8;
color: #A0AEC0;
}
.org-compound-input {
@@ -21256,6 +21423,10 @@ input[type=checkbox]:checked + .custom-checkbox {
gap: 8px;
width: 100%;
}
.org-compound-input.phone-input-group {
flex: none;
width: auto;
}
.org-compound-input .org-form-input,
.org-compound-input .org-form-select {
flex: 1;
@@ -21397,18 +21568,23 @@ input[type=checkbox]:checked + .custom-checkbox {
}
.org-btn-check, .org-file-label {
padding: 8px 16px;
padding: 10px 20px;
width: auto;
min-width: 100px;
background: #A4D6EA;
background: #00A1D7;
color: #FFFFFF;
border: 1px solid #A4D6EA;
border: 1px solid #00A1D7;
border-radius: 8px;
font-family: "Spoqa Han Sans Neo", "SpoqaHanSans", "Noto Sans KR", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 14px;
font-weight: 700;
text-align: center;
justify-content: center;
transition: all 0.2s ease;
}
.org-btn-check:not(:disabled):hover, .org-file-label:not(:disabled):hover {
background: #008BC0;
border-color: #008BC0;
color: #FFFFFF;
}
@media (max-width: 768px) {
@@ -21503,10 +21679,9 @@ input[type=checkbox]:checked + .custom-checkbox {
.org-action-buttons {
display: flex;
gap: 24px;
justify-content: center;
justify-content: end;
margin-top: 64px;
padding-top: 48px;
border-top: 1px solid #E2E8F0;
}
.org-action-buttons .btn {
width: 220px;
@@ -21558,14 +21733,17 @@ input[type=checkbox]:checked + .custom-checkbox {
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
display: none;
}
.org-validation-message.success {
background: rgba(107, 207, 127, 0.1);
color: #6BCF7F;
display: block;
}
.org-validation-message.error {
background: rgba(255, 107, 107, 0.1);
color: #FF6B6B;
display: block;
}
.org-loading-overlay {
@@ -21746,13 +21924,14 @@ input[type=checkbox]:checked + .custom-checkbox {
.agreement-content {
margin-top: 16px;
background: #EDF9FE;
background: #f9f9f9;
border-radius: 10px;
padding: 22px;
}
.agreement-scroll {
max-height: 300px;
max-height: 400px;
overflow-y: auto;
padding: 24px;
}
.agreement-scroll::-webkit-scrollbar {
width: 8px;
@@ -21773,7 +21952,12 @@ input[type=checkbox]:checked + .custom-checkbox {
line-height: 1.8;
color: #64748B;
}
.agreement-scroll div h1, .agreement-scroll div h2, .agreement-scroll div h3, .agreement-scroll div h4, .agreement-scroll div h5, .agreement-scroll div h6 {
.agreement-scroll div h1,
.agreement-scroll div h2,
.agreement-scroll div h3,
.agreement-scroll div h4,
.agreement-scroll div h5,
.agreement-scroll div h6 {
color: #1A1A2E;
margin-top: 24px;
margin-bottom: 16px;
@@ -21782,7 +21966,8 @@ input[type=checkbox]:checked + .custom-checkbox {
.agreement-scroll div p {
margin-bottom: 16px;
}
.agreement-scroll div ul, .agreement-scroll div ol {
.agreement-scroll div ul,
.agreement-scroll div ol {
margin-left: 24px;
margin-bottom: 16px;
}
@@ -21791,7 +21976,8 @@ input[type=checkbox]:checked + .custom-checkbox {
border-collapse: collapse;
margin-bottom: 16px;
}
.agreement-scroll div table th, .agreement-scroll div table td {
.agreement-scroll div table th,
.agreement-scroll div table td {
border: 1px solid #E2E8F0;
padding: 8px;
text-align: left;
@@ -21827,6 +22013,25 @@ input[type=checkbox]:checked + .custom-checkbox {
padding: 16px;
}
}
.register-card-wrapper {
background: #FFFFFF;
border: 1px solid rgba(226, 232, 240, 0.8);
border-radius: 20px;
padding: 48px 40px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.03);
margin-top: 30px;
margin-bottom: 60px;
width: 100%;
}
@media (max-width: 768px) {
.register-card-wrapper {
padding: 24px 16px;
border-radius: 12px;
margin-top: 15px;
margin-bottom: 40px;
}
}
.page-header {
margin-bottom: 64px;
text-align: center;
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -598,6 +598,14 @@
color: #fff;
}
&:disabled {
background: #cbd5e1 !important;
color: #94a3b8 !important;
cursor: not-allowed !important;
transform: none !important;
pointer-events: none !important;
}
&.md {
font-size: 14px;
padding: 11px 45px;
@@ -29,15 +29,15 @@
line-height: 1;
&::before {
content: "\2022"; // •
content: "";
}
}
&.is-idle {
color: #888;
color: #718096;
.policy-icon {
color: #b5b5b5;
color: #a0aec0;
}
}
@@ -71,5 +71,5 @@
margin: 10px 0 0 0;
font-size: 14px;
line-height: 18px;
color: #888;
}
color: #ef5555;
}
+1
View File
@@ -34,6 +34,7 @@
@use 'components/partners' as *;
@use 'components/cta' as *;
@use 'components/password-popup' as *;
@use 'components/password-policy' as *;
@use 'components/header-auth' as *;
@use 'components/session-timer' as *;
@use 'components/tables' as *;
@@ -90,7 +90,7 @@
background: none;
padding: 0 0 $spacing-md 0;
border-radius: 0;
border-bottom: 2px solid #212529;
border-bottom: 1px solid #212529;
margin-bottom: $spacing-xl;
h3 {
@@ -124,83 +124,78 @@
// Info Notice
.org-info-notice {
border-radius: $border-radius-md;
margin-bottom: $spacing-md;
background: #FFF5F5;
border: 1px solid #FED7D7;
border-radius: 8px;
padding: 14px 20px;
display: flex;
align-items: center;
gap: 12px;
margin-top: $spacing-md;
margin-bottom: $spacing-xl;
ul {
list-style: none;
padding: 0;
.notice-icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
color: #E53E3E;
flex-shrink: 0;
}
.notice-text {
font-family: $font-family-primary;
font-size: 14px;
font-weight: 500;
color: #C53030;
line-height: 1.5;
margin: 0;
li {
position: relative;
padding-left: $spacing-lg;
color: $text-gray;
font-size: $font-size-sm;
line-height: 1.6;
&::before {
content: '';
position: absolute;
left: 0;
color: $primary-blue;
font-weight: $font-weight-bold;
}
& + li {
margin-top: $spacing-sm;
}
}
}
}
// Form Groups
.org-form-group {
margin-bottom: 16px;
margin-bottom: 26px;
display: flex;
align-items: flex-start;
gap: 20px;
flex-direction: column;
align-items: stretch;
gap: 13px;
&:last-child {
margin-bottom: 0;
}
@include respond-to('sm') {
flex-direction: column;
gap: 10px;
margin-bottom: 12px;
}
}
.org-form-label {
display: flex;
align-items: center;
gap: $spacing-sm;
font-size: 15px;
font-weight: $font-weight-regular;
color: $text-dark;
min-width: 160px;
padding-top: 10px;
font-family: $font-family-primary;
font-size: 16px;
font-weight: 700;
color: #1A202C;
width: auto;
margin: 0;
padding: 0;
.required-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 2px 8px;
background: #3BA4ED;
color: $white;
font-size: 12px;
font-weight: $font-weight-medium;
border-radius: 4px;
line-height: 1.2;
}
visibility: hidden;
position: relative;
width: 10px;
@include respond-to('sm') {
min-width: auto;
padding-top: 0;
&::after {
content: '*';
visibility: visible;
position: absolute;
left: 4px;
top: 0;
color: #f4253c;
font-size: 16px;
font-weight: 700;
}
}
}
.org-form-input-wrapper {
flex: 1;
display: flex;
@@ -217,6 +212,8 @@
display: flex;
align-items: center;
gap: $spacing-md;
width: 100%;
max-width: 650px;
.org-form-input,
.org-compound-input,
@@ -249,18 +246,20 @@
// Input Fields
.org-form-input {
width: 100%;
padding: 8px 12px;
border: 1px solid $border-gray;
border-radius: $border-radius-md;
padding: 10px 16px;
border: 1px solid #CBD5E0;
border-radius: 8px;
font-family: $font-family-primary;
font-size: 14px;
color: $text-dark;
transition: $transition-base;
background: $white;
color: #1A202C;
transition: all 0.3s ease;
background: #FCFDFD;
&:focus {
outline: none;
border-color: $primary-blue;
box-shadow: 0 0 0 3px rgba(75, 155, 255, 0.1);
border-color: #0049B4; // Brand primary blue
box-shadow: 0 0 0 3px rgba(0, 73, 180, 0.15);
background: #FFFFFF;
}
&:disabled {
@@ -278,7 +277,7 @@
}
&::placeholder {
color: $text-light;
color: #A0AEC0;
}
}
@@ -289,6 +288,11 @@
gap: $spacing-sm;
width: 100%;
&.phone-input-group {
flex: none;
width: auto;
}
.org-form-input,
.org-form-select {
flex: 1;
@@ -448,18 +452,23 @@
}
.org-btn-check {
padding: 8px 16px;
padding: 10px 20px;
width: auto;
min-width: 100px;
background: #A4D6EA;
background: #00A1D7;
color: $white;
border: 1px solid #A4D6EA;
border-radius: $border-radius-md;
border: 1px solid #00A1D7;
border-radius: 8px;
font-family: $font-family-primary;
font-size: 14px;
font-weight: 700;
text-align: center;
justify-content: center;
transition: all 0.2s ease;
&:not(:disabled):hover {
background: #008BC0;
border-color: #008BC0;
color: $white;
}
@@ -548,7 +557,7 @@
color: $text-gray;
line-height: 1.5;
& + p {
&+p {
margin-top: $spacing-xs;
}
@@ -564,12 +573,11 @@
.org-action-buttons {
display: flex;
gap: $spacing-lg;
justify-content: center;
justify-content: end;
margin-top: $spacing-4xl;
padding-top: $spacing-3xl;
border-top: 1px solid $border-gray;
.btn {
.btn {
width: 220px;
text-align: center;
justify-content: center;
@@ -627,15 +635,18 @@
padding: $spacing-sm $spacing-md;
border-radius: $border-radius-sm;
font-size: $font-size-sm;
display: none;
&.success {
background: rgba(107, 207, 127, 0.1);
color: $accent-green;
display: block;
}
&.error {
background: rgba(255, 107, 107, 0.1);
color: $accent-orange;
display: block;
}
}
@@ -758,11 +769,11 @@
opacity: 0;
pointer-events: none;
&:checked + .agreement-checkbox-custom {
&:checked+.agreement-checkbox-custom {
background-image: url('/img/checkbox-checked.png');
}
&:focus + .agreement-checkbox-custom {
&:focus+.agreement-checkbox-custom {
box-shadow: 0 0 0 3px rgba(140, 149, 159, 0.1);
}
}
@@ -847,13 +858,15 @@
// Agreement Content
.agreement-content {
margin-top: $spacing-md;
background: $main-bg;
background: #f9f9f9;
border-radius: 10px;
padding: 22px;
}
.agreement-scroll {
max-height: 300px;
max-height: 400px;
overflow-y: auto;
padding: $spacing-lg;
// padding: $spacing-lg;
&::-webkit-scrollbar {
width: 8px;
@@ -879,7 +892,12 @@
line-height: 1.8;
color: $text-gray;
h1, h2, h3, h4, h5, h6 {
h1,
h2,
h3,
h4,
h5,
h6 {
color: $text-dark;
margin-top: $spacing-lg;
margin-bottom: $spacing-md;
@@ -890,7 +908,8 @@
margin-bottom: $spacing-md;
}
ul, ol {
ul,
ol {
margin-left: $spacing-lg;
margin-bottom: $spacing-md;
}
@@ -900,7 +919,8 @@
border-collapse: collapse;
margin-bottom: $spacing-md;
th, td {
th,
td {
border: 1px solid $border-gray;
padding: $spacing-sm;
text-align: left;
@@ -947,3 +967,22 @@
padding: $spacing-md;
}
}
// Premium card wrapper for registration forms
.register-card-wrapper {
background: #FFFFFF;
border: 1px solid rgba(226, 232, 240, 0.8);
border-radius: 20px;
padding: 48px 40px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.03);
margin-top: 30px;
margin-bottom: 60px;
width: 100%;
@include respond-to('sm') {
padding: 24px 16px;
border-radius: 12px;
margin-top: 15px;
margin-bottom: 40px;
}
}
@@ -7,16 +7,14 @@
// -----------------------------------------------------------------------------
.signup-selection-page {
min-height: calc(100vh - 380px); // Account for header and footer
display: flex;
align-items: center;
justify-content: center;
background: #EDF9FE; // Light blue background from Figma (same as login)
padding: 30px 20px;
padding: 100px 20px;
position: relative;
border-radius: 12px;
margin-top: 30px;
margin-bottom: 30px;
margin-top: 60px;
margin-bottom: 50px;
}
.signup-selection-container {
@@ -38,118 +36,225 @@
}
}
.signup-logo {
width: 90px;
height: 90px;
margin-bottom: 15px;
.signup-header {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 40px;
width: 100%;
}
img {
width: 100%;
height: 100%;
object-fit: contain;
.signup-title-row {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin-bottom: 12px;
color: #000000;
.signup-title-icon {
color: #000000;
flex-shrink: 0;
}
.signup-title {
font-family: $font-family-primary;
font-size: 36px;
font-weight: 700;
color: #000000;
margin: 0;
letter-spacing: -0.5px;
}
}
.signup-message {
font-family: $font-family-primary;
font-size: 20px;
font-size: 16px;
font-weight: 400;
color: #000000;
color: #718096;
text-align: center;
margin-bottom: 20px;
line-height: 1;
margin: 0;
line-height: 1.5;
}
.signup-buttons {
.signup-cards {
width: 100%;
max-width: 504px;
max-width: 760px;
display: flex;
flex-direction: row;
gap: 28px;
margin-bottom: 45px;
justify-content: center;
}
.signup-card-item {
flex: 1;
background: #FFFFFF;
border: 1px solid rgba(226, 232, 240, 0.8);
border-radius: 24px;
padding: 45px 30px;
display: flex;
flex-direction: column;
gap: 20px;
margin-bottom: 30px;
}
.signup-btn {
width: 100%;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 10px;
font-family: $font-family-primary;
font-size: 20px;
font-weight: 700;
color: #FFFFFF;
text-align: center;
text-decoration: none;
border-radius: 12px;
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
color: #1A202C;
cursor: pointer;
transition: all 0.3s ease;
line-height: 1;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.03);
position: relative;
overflow: hidden;
// Background subtle glow effect on hover
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at top right, rgba(255, 255, 255, 0.8), transparent 70%);
opacity: 0;
transition: opacity 0.4s ease;
pointer-events: none;
}
.card-icon-wrapper {
width: 76px;
height: 76px;
border-radius: 22px; // Squircle style
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24px;
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.02);
}
.card-text-wrapper {
display: flex;
flex-direction: column;
align-items: center;
}
.card-title {
font-family: $font-family-primary;
font-size: 22px;
font-weight: 800;
margin-top: 0;
margin-bottom: 12px;
line-height: 1.3;
color: #1A202C;
transition: all 0.4s ease;
}
.card-desc {
font-family: $font-family-primary;
font-size: 14px;
color: #718096;
line-height: 1.6;
margin: 0;
max-width: 250px;
word-break: keep-all;
transition: color 0.4s ease;
}
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
transform: translateY(-8px);
background: #FFFFFF;
&:active {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
.signup-btn-icon {
width: 26px;
height: 30px;
flex-shrink: 0;
}
span {
white-space: nowrap;
}
}
.signup-btn-individual {
background: #0049B4;
}
.signup-btn-organization {
background: #00A1D7;
}
.signup-navigation {
display: flex;
justify-content: center;
align-items: center;
gap: 26px;
flex-wrap: wrap;
.signup-nav-link {
font-family: $font-family-primary;
font-size: 16px;
font-weight: 400;
color: #000000;
text-decoration: none;
transition: color 0.3s ease;
line-height: 1;
&:hover {
color: #0049B4;
text-decoration: underline;
&::before {
opacity: 1;
}
}
.signup-nav-separator {
color: #000000;
font-size: 16px;
line-height: 1;
&.individual-card {
.card-icon-wrapper {
background: linear-gradient(135deg, #F0F5FF 0%, #D8E5FF 100%);
color: #0049B4;
}
&:hover {
border-color: #0049B4;
box-shadow: 0 20px 40px rgba(0, 73, 180, 0.12);
.card-title {
color: #0049B4;
}
.card-icon-wrapper {
background: linear-gradient(135deg, #0049B4 0%, #0066FF 100%);
color: #FFFFFF;
transform: scale(1.05) rotate(3deg);
box-shadow: 0 10px 20px rgba(0, 73, 180, 0.25);
}
}
}
&.organization-card {
.card-icon-wrapper {
background: linear-gradient(135deg, #EBF8FF 0%, #CAF0F8 100%);
color: #00A1D7;
}
&:hover {
border-color: #00A1D7;
box-shadow: 0 20px 40px rgba(0, 161, 215, 0.12);
.card-title {
color: #00A1D7;
}
.card-icon-wrapper {
background: linear-gradient(135deg, #00A1D7 0%, #33C2FF 100%);
color: #FFFFFF;
transform: scale(1.05) rotate(-3deg);
box-shadow: 0 10px 20px rgba(0, 161, 215, 0.25);
}
}
}
}
.signup-navigation-box {
display: flex;
align-items: center;
background: #EDF4FF;
border: 1px solid #0049B4;
border-radius: 8px;
overflow: hidden;
height: 42px;
max-width: 280px;
width: 100%;
margin: 0 auto;
.signup-nav-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font-family: $font-family-primary;
font-size: 14px;
font-weight: 700;
color: #0049B4;
text-decoration: none;
height: 100%;
transition: all 0.2s ease;
&:hover {
background: #DCE7FF;
}
}
.signup-nav-divider {
width: 1px;
height: 100%;
background: #0049B4;
}
}
// Responsive adjustments - Figma Mobile Design (sm: 768px)
@include respond-to('sm') {
.signup-selection-page {
// Figma: 전체 배경 #ebfbff, 풀 높이
background: #ebfbff;
min-height: calc(100vh - 44px);
padding: 40px 20px;
@@ -163,73 +268,83 @@
padding: 0;
}
.signup-selection-card {
padding: 0;
}
.signup-header {
margin-bottom: 30px;
.signup-logo {
// Figma: 64px × 64px
width: 64px;
height: 64px;
margin-bottom: 24px;
.signup-title-row {
margin-bottom: 8px;
.signup-title-icon {
width: 24px;
height: 24px;
}
.signup-title {
font-size: 24px;
}
}
}
.signup-message {
// Figma: 14px Regular, #212529
font-size: 14px;
font-weight: 400;
color: #212529;
margin-bottom: 80px;
line-height: 26px;
margin-bottom: 40px;
line-height: 22px;
}
.signup-buttons {
// Figma: 버튼 간격 24px
.signup-cards {
max-width: 335px;
gap: 24px;
margin-bottom: 80px;
flex-direction: column;
gap: 16px;
margin-bottom: 40px;
}
.signup-btn {
// Figma: 335px × 44px, border-radius 8px
height: 44px;
font-size: 16px;
font-weight: 700;
border-radius: 8px;
padding: 8px 12px;
justify-content: center;
gap: 8px;
.signup-card-item {
padding: 24px 20px;
border-radius: 12px;
flex-direction: row;
text-align: left;
align-items: center;
gap: 16px;
svg {
width: 14px;
height: 16px;
.card-icon-wrapper {
width: 44px;
height: 44px;
margin-bottom: 0;
flex-shrink: 0;
svg {
width: 20px;
height: 20px;
}
}
span {
text-align: center;
.card-text-wrapper {
align-items: flex-start;
}
.card-title {
font-size: 16px;
margin-bottom: 4px;
}
.card-desc {
font-size: 12px;
}
&:hover {
transform: none;
box-shadow: none;
opacity: 0.9;
}
}
.signup-navigation {
// Figma: 14px Regular, #515961, gap 12px
gap: 12px;
.signup-navigation-box {
max-width: 260px;
height: 38px;
.signup-nav-link {
font-size: 14px;
font-weight: 400;
color: #515961;
}
.signup-nav-separator {
font-size: 14px;
color: #515961;
.signup-nav-btn {
font-size: 13px;
}
}
}
}
@@ -1,218 +1,207 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<body>
<th:block th:fragment="commonUserInfo">
<div class="form-row">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">이메일 아이디</span>
</div>
<div class="form-field-wrapper">
<input type="text" th:value="${loginId}" name="loginId" class="form-input input-readonly"
placeholder="이메일" disabled="disabled">
<input type="hidden" th:value="${loginId}" name="loginId">
</div>
</div>
<div class="form-row">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">성명</span> <span class="required-badge">필수</span>
<body>
<th:block th:fragment="commonUserInfo">
<div class="form-row">
<div class="form-label-wrapper">
<span class="form-label-text">이메일 아이디</span>
</div>
<div class="form-field-wrapper">
<input type="text" th:value="${loginId}" name="loginId" class="form-input input-readonly"
placeholder="이메일" disabled="disabled">
<input type="hidden" th:value="${loginId}" name="loginId">
</div>
</div>
<div class="form-field-wrapper">
<input type="text" th:value="${userName}" id="userName" name="userName" class="form-input"
placeholder="성명">
<div class="form-row">
<div class="form-label-wrapper">
<span class="form-label-text">성명</span> <span class="required-badge">필수</span>
</div>
<div class="form-field-wrapper">
<input type="text" th:value="${userName}" id="userName" name="userName" class="form-input"
placeholder="성명">
</div>
</div>
</div>
<div class="form-row">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">휴대폰 번호</span> <span class="required-badge">필수</span>
</div>
<div class="form-field-wrapper input-with-button"
th:with="hasHyphen=${mobileNumber != null and #strings.contains(mobileNumber, '-')},
<div class="form-row">
<div class="form-label-wrapper">
<span class="form-label-text">휴대폰 번호</span> <span class="required-badge">필수</span>
</div>
<div class="form-field-wrapper input-with-button"
th:with="hasHyphen=${mobileNumber != null and #strings.contains(mobileNumber, '-')},
parts=${hasHyphen ? #strings.arraySplit(mobileNumber, '-') : null},
len=${mobileNumber != null ? #strings.length(mobileNumber) : 0},
mobilePrefix=${hasHyphen ? (parts != null and #arrays.length(parts) > 0 ? parts[0] : '') : (len >= 3 ? #strings.substring(mobileNumber, 0, 3) : '')},
mobileMiddle=${hasHyphen ? (parts != null and #arrays.length(parts) > 1 ? parts[1] : '') : (len == 11 ? #strings.substring(mobileNumber, 3, 7) : (len == 10 ? #strings.substring(mobileNumber, 3, 6) : ''))},
mobileLast=${hasHyphen ? (parts != null and #arrays.length(parts) > 2 ? parts[2] : '') : (len == 11 ? #strings.substring(mobileNumber, 7, 11) : (len == 10 ? #strings.substring(mobileNumber, 6, 10) : ''))}">
<div class="compound-input">
<input type="text"
th:value="${mobilePrefix}"
name="mobilePrefix"
class="form-input input-readonly"
maxlength="3"
disabled="disabled">
<span class="separator">-</span>
<input type="text"
th:value="${mobileMiddle}"
name="mobileMiddle"
class="form-input input-readonly"
maxlength="4"
disabled="disabled">
<span class="separator">-</span>
<input type="text"
th:value="${mobileLast}"
name="mobileLast"
class="form-input input-readonly"
maxlength="4"
disabled="disabled">
<div class="compound-input">
<input type="text" th:value="${mobilePrefix}" name="mobilePrefix" class="form-input input-readonly"
maxlength="3" disabled="disabled">
<span class="separator">-</span>
<input type="text" th:value="${mobileMiddle}" name="mobileMiddle" class="form-input input-readonly"
maxlength="4" disabled="disabled">
<span class="separator">-</span>
<input type="text" th:value="${mobileLast}" name="mobileLast" class="form-input input-readonly"
maxlength="4" disabled="disabled">
</div>
<button type="button" class="btn-input-action btn-change change_mobile_phone"
id="changePhoneBtn">변경</button>
</div>
<button type="button" class="btn-input-action btn-change change_mobile_phone" id="changePhoneBtn">변경</button>
</div>
</div>
<div class="form-row" id="newPhoneNumberContainer" style="display: none;">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">새 휴대폰 번호</span> <span class="required-badge">필수</span>
</div>
<div class="form-field-wrapper input-with-button">
<div class="compound-input">
<select class="form-select phone-number" id="phoneMobile" name="newMobilePrefix">
<option value="">선택</option>
<option value="010">010</option>
<option value="011">011</option>
<option value="016">016</option>
<option value="017">017</option>
<option value="018">018</option>
<option value="019">019</option>
</select>
<span class="separator">-</span>
<input type="text" id="newMobileMiddle" name="newMobileMiddle" class="form-input phone-number"
maxlength="4" pattern="\d*" inputmode="numeric" placeholder="앞자리"/>
<span class="separator">-</span>
<input type="text" id="newMobileLast" name="newMobileLast" class="form-input phone-number"
maxlength="4" pattern="\d*" inputmode="numeric" placeholder="뒷자리"/>
<input type="hidden" value="" name="mobileNumber" id="mobileNumber">
<div class="form-row" id="newPhoneNumberContainer" style="display: none;">
<div class="form-label-wrapper">
<span class="form-label-text">새 휴대폰 번호</span> <span class="required-badge">필수</span>
</div>
<button type="button" class="btn-input-action btn-auth btn_auth">
인증번호 받기
</button>
</div>
</div>
<div class="form-row" id="authNumberContainer" style="display: none;">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">인증번호 입력</span> <span class="required-badge">필수</span>
</div>
<div class="form-field-wrapper input-with-button">
<div class="auth-input-group">
<input type="text" id="authNumber" name="authNumber" maxlength="6" pattern="[0-9]*"
inputmode="numeric" class="form-input"
placeholder="SMS 인증번호 6자리">
<span class="auth-timer" id="certify_time">03:00</span>
<div class="form-field-wrapper input-with-button">
<div class="compound-input">
<select class="form-select phone-number" id="phoneMobile" name="newMobilePrefix">
<option value="">선택</option>
<option value="010">010</option>
<option value="011">011</option>
<option value="016">016</option>
<option value="017">017</option>
<option value="018">018</option>
<option value="019">019</option>
</select>
<span class="separator">-</span>
<input type="text" id="newMobileMiddle" name="newMobileMiddle" class="form-input phone-number"
maxlength="4" pattern="\d*" inputmode="numeric" placeholder="앞자리" />
<span class="separator">-</span>
<input type="text" id="newMobileLast" name="newMobileLast" class="form-input phone-number"
maxlength="4" pattern="\d*" inputmode="numeric" placeholder="뒷자리" />
<input type="hidden" value="" name="mobileNumber" id="mobileNumber">
</div>
<button type="button" class="btn-input-action btn-auth btn_auth ">
인증번호 받기
</button>
</div>
<button type="button" class="btn-input-action btn-auth btn_verify_auth">
인증번호 확인
</button>
</div>
</div>
<div id="authNumber-validation"></div>
</th:block>
<th:block th:fragment="commonUserInfoScript">
<script>
// 휴대폰 번호 조합 함수
function combineMobileNumber() {
let mobileSelect = document.getElementById('phoneMobile');
if (!mobileSelect) {
console.error('Mobile select element not found');
return null;
}
let prefix = mobileSelect.value;
let middle = document.getElementById('newMobileMiddle')?.value.trim() || '';
let last = document.getElementById('newMobileLast')?.value.trim() || '';
console.log('values:', {prefix, middle, last});
if (!prefix || !middle || !last) {
return null;
}
// 하이픈을 포함한 번호 조합
let combinedNumber = `${prefix}-${middle}-${last}`;
let mobileNumberInput = document.getElementById('mobileNumber');
if (mobileNumberInput) {
mobileNumberInput.value = combinedNumber;
console.log('set combined number:', combinedNumber);
}
return combinedNumber;
}
// 전역 변수로 설정
window.isAuthVerified = false;
document.addEventListener('DOMContentLoaded', function () {
let countdownInterval;
let isAuthVerified = false;
// 휴대폰 번호 파싱 함수 추가
function parseMobileNumber(mobileNumber) {
if (!mobileNumber) return null;
// "-" 포함하는 전화번호 형식 파싱
const parts = mobileNumber.split('-');
if (parts.length === 3) {
return {
prefix: parts[0],
middle: parts[1],
last: parts[2]
};
<div class="form-row" id="authNumberContainer" style="display: none;">
<div class="form-label-wrapper">
<span class="form-label-text">인증번호 입력</span> <span class="required-badge">필수</span>
</div>
<div class="form-field-wrapper input-with-button">
<div class="auth-input-group">
<input type="text" id="authNumber" name="authNumber" maxlength="6" pattern="[0-9]*"
inputmode="numeric" class="form-input" placeholder="SMS 인증번호 6자리">
<span class="auth-timer" id="certify_time">03:00</span>
</div>
<button type="button" class="btn-input-action btn-auth btn_verify_auth">
인증번호 확인
</button>
</div>
</div>
<div id="authNumber-validation"></div>
</th:block>
<th:block th:fragment="commonUserInfoScript">
<script>
// 휴대폰 번호 조합 함수
function combineMobileNumber() {
let mobileSelect = document.getElementById('phoneMobile');
if (!mobileSelect) {
console.error('Mobile select element not found');
return null;
}
// 하이픈이 없는 경우
if (mobileNumber.length === 11) {
return {
prefix: mobileNumber.substring(0, 3),
middle: mobileNumber.substring(3, 7),
last: mobileNumber.substring(7)
};
let prefix = mobileSelect.value;
let middle = document.getElementById('newMobileMiddle')?.value.trim() || '';
let last = document.getElementById('newMobileLast')?.value.trim() || '';
console.log('values:', { prefix, middle, last });
if (!prefix || !middle || !last) {
return null;
}
return null;
// 하이픈을 포함한 번호 조합
let combinedNumber = `${prefix}-${middle}-${last}`;
let mobileNumberInput = document.getElementById('mobileNumber');
if (mobileNumberInput) {
mobileNumberInput.value = combinedNumber;
console.log('set combined number:', combinedNumber);
}
return combinedNumber;
}
// 초기 휴대폰 번호 설정
function initializeMobileNumber() {
const mobileNumber = document.querySelector('[name="mobileNumber"]')?.value;
if (mobileNumber) {
const parsed = parseMobileNumber(mobileNumber);
if (parsed) {
// 기존 번호 입력 필드 설정
document.querySelector('[name="mobilePrefix"]').value = parsed.prefix;
document.querySelector('[name="mobileMiddle"]').value = parsed.middle;
document.querySelector('[name="mobileLast"]').value = parsed.last;
// 전역 변수로 설정
window.isAuthVerified = false;
document.addEventListener('DOMContentLoaded', function () {
let countdownInterval;
let isAuthVerified = false;
// 휴대폰 번호 파싱 함수 추가
function parseMobileNumber(mobileNumber) {
if (!mobileNumber) return null;
// "-" 포함하는 전화번호 형식 파싱
const parts = mobileNumber.split('-');
if (parts.length === 3) {
return {
prefix: parts[0],
middle: parts[1],
last: parts[2]
};
}
// 하이픈이 없는 경우
if (mobileNumber.length === 11) {
return {
prefix: mobileNumber.substring(0, 3),
middle: mobileNumber.substring(3, 7),
last: mobileNumber.substring(7)
};
}
return null;
}
// 초기 휴대폰 번호 설정
function initializeMobileNumber() {
const mobileNumber = document.querySelector('[name="mobileNumber"]')?.value;
if (mobileNumber) {
const parsed = parseMobileNumber(mobileNumber);
if (parsed) {
// 기존 번호 입력 필드 설정
document.querySelector('[name="mobilePrefix"]').value = parsed.prefix;
document.querySelector('[name="mobileMiddle"]').value = parsed.middle;
document.querySelector('[name="mobileLast"]').value = parsed.last;
}
}
}
}
// 전화번호 입력 필드 숫자만 입력 가능
$('#newMobileMiddle, #newMobileLast, #authNumber').on('input', function () {
$(this).val($(this).val().replace(/[^0-9]/g, ''));
});
// 전화번호 입력 필드 숫자만 입력 가능
$('#newMobileMiddle, #newMobileLast, #authNumber').on('input', function () {
$(this).val($(this).val().replace(/[^0-9]/g, ''));
});
// select 변경 이벤트
$('#phoneMobile').on('change', function () {
combineMobileNumber();
});
// select 변경 이벤트
$('#phoneMobile').on('change', function () {
combineMobileNumber();
});
// 인증번호 검증 결과 처리 함수
function handleAuthNumberVerification(response) {
customPopups.showAlert(response.message);
if (response.valid) {
let authNumberInput = document.getElementById('authNumber');
let verifyAuthButton = document.querySelector(".btn_verify_auth");
let timerElement = document.getElementById('certify_time');
// 인증번호 검증 결과 처리 함수
function handleAuthNumberVerification(response) {
customPopups.showAlert(response.message);
if (response.valid) {
let authNumberInput = document.getElementById('authNumber');
let verifyAuthButton = document.querySelector(".btn_verify_auth");
let timerElement = document.getElementById('certify_time');
if (authNumberInput) authNumberInput.disabled = true;
if (verifyAuthButton) verifyAuthButton.disabled = true;
clearInterval(countdownInterval);
if (timerElement) timerElement.textContent = '인증 완료';
window.isAuthVerified = true; // 전역 변수로 설정
document.dispatchEvent(new Event('authVerified'));
if (authNumberInput) authNumberInput.disabled = true;
if (verifyAuthButton) verifyAuthButton.disabled = true;
clearInterval(countdownInterval);
if (timerElement) timerElement.textContent = '인증 완료';
window.isAuthVerified = true; // 전역 변수로 설정
document.dispatchEvent(new Event('authVerified'));
}
}
}
// 테스트 환경 안내 메시지 표시
function displayTestAuthNotice(authNumber) {
const noticeHtml = `
// 테스트 환경 안내 메시지 표시
function displayTestAuthNotice(authNumber) {
const noticeHtml = `
<div class="test-env-notice" style="margin-top: 12px; padding: 12px 16px; background-color: #FFF4E6; border: 1px solid #FFB84D; border-radius: 4px;">
<div class="test-env-notice__content" style="display: flex; align-items: flex-start; gap: 8px;">
<svg class="test-env-notice__icon" style="flex-shrink: 0; margin-top: 2px; width: 20px; height: 20px;" viewBox="0 0 20 20" fill="none">
@@ -230,184 +219,185 @@
</div>
`;
// 기존 안내 메시지 제거
$('.test-env-notice').remove();
// 기존 안내 메시지 제거
$('.test-env-notice').remove();
// 인증번호 container의 이전 위치에 안내 메시지 삽입
const authContainer = document.getElementById('authNumberContainer');
if (authContainer) {
const noticeDiv = document.createElement('div');
noticeDiv.className = 'form-row';
noticeDiv.innerHTML = `<div class="form-label-wrapper label-offset"></div><div class="form-field-wrapper">${noticeHtml}</div>`;
authContainer.parentNode.insertBefore(noticeDiv, authContainer);
}
}
// 인증번호 요청 후 처리 함수
function handleAuthNumberRequest(response) {
if (response.valid) {
// 테스트 환경 인증번호 표시
if (response.authNumber) {
displayTestAuthNotice(response.authNumber);
// 인증번호 container의 이전 위치에 안내 메시지 삽입
const authContainer = document.getElementById('authNumberContainer');
if (authContainer) {
const noticeDiv = document.createElement('div');
noticeDiv.className = 'form-row';
noticeDiv.innerHTML = `<div class="form-label-wrapper label-offset"></div><div class="form-field-wrapper">${noticeHtml}</div>`;
authContainer.parentNode.insertBefore(noticeDiv, authContainer);
}
let authButton = document.querySelector(".btn_auth");
if (authButton) authButton.disabled = true;
startAuthNumberTimer();
customPopups.showAlert(response.message || "인증번호가 발송되었습니다.");
} else {
customPopups.showAlert(response.message || '알 수 없는 오류가 발생했습니다.');
let authButton = document.querySelector(".btn_auth");
if (authButton) authButton.disabled = false;
}
}
// 인증번호 타이머 시작 함수
function startAuthNumberTimer() {
clearInterval(countdownInterval);
let remainingTime = 180; // 3분
let timerElement = document.getElementById("certify_time");
let authButton = document.querySelector(".btn_auth");
let verifyAuthButton = document.querySelector(".btn_verify_auth");
let authNumberInput = document.getElementById('authNumber');
// 인증번호 요청 후 처리 함수
function handleAuthNumberRequest(response) {
if (response.valid) {
// 테스트 환경 인증번호 표시
if (response.authNumber) {
displayTestAuthNotice(response.authNumber);
}
function updateRemainingTime() {
if (remainingTime <= 0) {
clearInterval(countdownInterval);
if (timerElement) timerElement.textContent = "입력 시간이 초과되었습니다.";
customPopups.showAlert("입력 시간이 초과되었습니다. 인증번호를 다시 발송해주세요.");
if (authButton) authButton.disabled = false;
if (verifyAuthButton) verifyAuthButton.disabled = true;
if (authNumberInput) authNumberInput.readOnly = true;
let authButton = document.querySelector(".btn_auth");
if (authButton) authButton.disabled = true;
startAuthNumberTimer();
customPopups.showAlert(response.message || "인증번호가 발송되었습니다.");
} else {
let minutes = Math.floor(remainingTime / 60);
let seconds = remainingTime % 60;
if (timerElement) timerElement.textContent = `남은 시간: ${minutes}:${seconds.toString().padStart(2, '0')}`;
remainingTime--;
customPopups.showAlert(response.message || '알 수 없는 오류가 발생했습니다.');
let authButton = document.querySelector(".btn_auth");
if (authButton) authButton.disabled = false;
}
}
updateRemainingTime();
countdownInterval = setInterval(updateRemainingTime, 1000);
}
// 인증번호 타이머 시작 함수
function startAuthNumberTimer() {
clearInterval(countdownInterval);
let remainingTime = 180; // 3분
let timerElement = document.getElementById("certify_time");
let authButton = document.querySelector(".btn_auth");
let verifyAuthButton = document.querySelector(".btn_verify_auth");
let authNumberInput = document.getElementById('authNumber');
// 이벤트 리스너 등록
document.querySelectorAll('#newMobileMiddle, #newMobileLast')
.forEach(el => el.addEventListener('input', combineMobileNumber));
let authNumberInput = document.getElementById("authNumber");
if (authNumberInput) {
authNumberInput.addEventListener("input", function () {
this.value = this.value.replace(/\D/g, '');
this.classList.toggle('is-invalid', this.value.length !== 6);
});
}
let userNameInput = document.getElementById("userName");
if (userNameInput) {
userNameInput.addEventListener("input", function () {
this.classList.toggle('is-invalid', this.value.trim().length === 0);
});
}
// 인증번호 요청 버튼 이벤트 리스너
let authButton = document.querySelector(".btn_auth");
if (authButton) {
authButton.addEventListener("click", function () {
// 먼저 새로운 휴대폰 번호 조합
let newMobileNumber = combineMobileNumber();
if (!newMobileNumber) {
customPopups.showAlert("휴대폰 번호를 올바르게 입력해주세요.");
return;
}
// 기존 휴대폰 번호 가져오기 (하이픈 없이)
const originalPrefix = document.querySelector('[name="mobilePrefix"]').value;
const originalMiddle = document.querySelector('[name="mobileMiddle"]').value;
const originalLast = document.querySelector('[name="mobileLast"]').value;
const originalMobileNumber = `${originalPrefix}${originalMiddle}${originalLast}`;
// 새 번호에서도 하이픈 제거하여 비교
const newMobileRaw = newMobileNumber.replace(/-/g, '');
// 동일한 번호인지 체크
if (newMobileRaw === originalMobileNumber) {
customPopups.showAlert("동일한 휴대폰 번호입니다.");
return;
}
console.log('Original Mobile:', originalMobileNumber);
console.log('New Mobile:', newMobileNumber);
// jQuery AJAX 호출
$.ajax({
url: /*[[@{/auth/request_auth_number}]]*/ '/auth/request_auth_number',
type: 'POST',
data: {
mobileNumber: newMobileNumber,
_csrf: $('input[name="_csrf"]').val()
},
success: function(response) {
handleAuthNumberRequest(response);
},
error: function(xhr, status, error) {
customPopups.showAlert("처리 중 오류가 발생했습니다. 다시 시도해주세요.");
function updateRemainingTime() {
if (remainingTime <= 0) {
clearInterval(countdownInterval);
if (timerElement) timerElement.textContent = "입력 시간이 초과되었습니다.";
customPopups.showAlert("입력 시간이 초과되었습니다. 인증번호를 다시 발송해주세요.");
if (authButton) authButton.disabled = false;
if (verifyAuthButton) verifyAuthButton.disabled = true;
if (authNumberInput) authNumberInput.readOnly = true;
} else {
let minutes = Math.floor(remainingTime / 60);
let seconds = remainingTime % 60;
if (timerElement) timerElement.textContent = `남은 시간: ${minutes}:${seconds.toString().padStart(2, '0')}`;
remainingTime--;
}
});
});
}
// 인증번호 확인 버튼 이벤트 리스너
let verifyAuthButton = document.querySelector(".btn_verify_auth");
if (verifyAuthButton) {
verifyAuthButton.addEventListener("click", function () {
let authNumber = document.getElementById('authNumber').value;
if (authNumber.length !== 6) {
customPopups.showAlert("6자리 인증번호를 입력해주세요.");
return;
}
let mobileNumber = document.getElementById('mobileNumber').value;
updateRemainingTime();
countdownInterval = setInterval(updateRemainingTime, 1000);
}
// jQuery AJAX 호출
$.ajax({
url: /*[[@{/auth/verify_auth_number}]]*/ '/auth/verify_auth_number',
type: 'POST',
data: {
mobileNumber: mobileNumber,
authNumber: authNumber,
_csrf: $('input[name="_csrf"]').val()
},
success: function(response) {
handleAuthNumberVerification(response);
},
error: function(xhr, status, error) {
customPopups.showAlert("처리 중 오류가 발생했습니다. 다시 시도해주세요.");
}
// 이벤트 리스너 등록
document.querySelectorAll('#newMobileMiddle, #newMobileLast')
.forEach(el => el.addEventListener('input', combineMobileNumber));
let authNumberInput = document.getElementById("authNumber");
if (authNumberInput) {
authNumberInput.addEventListener("input", function () {
this.value = this.value.replace(/\D/g, '');
this.classList.toggle('is-invalid', this.value.length !== 6);
});
});
}
}
// 휴대폰 번호 변경 버튼 이벤트 리스너
const changeMobilePhoneBtn = document.querySelector('.change_mobile_phone');
const newPhoneNumberContainer = document.getElementById('newPhoneNumberContainer');
const authNumberContainer = document.getElementById('authNumberContainer');
let userNameInput = document.getElementById("userName");
if (userNameInput) {
userNameInput.addEventListener("input", function () {
this.classList.toggle('is-invalid', this.value.trim().length === 0);
});
}
if (changeMobilePhoneBtn && newPhoneNumberContainer && authNumberContainer) {
changeMobilePhoneBtn.addEventListener('click', function (e) {
e.preventDefault();
// 인증번호 요청 버튼 이벤트 리스너
let authButton = document.querySelector(".btn_auth");
if (authButton) {
authButton.addEventListener("click", function () {
// 먼저 새로운 휴대폰 번호 조합
let newMobileNumber = combineMobileNumber();
if (!newMobileNumber) {
customPopups.showAlert("휴대폰 번호를 올바르게 입력해주세요.");
return;
}
newPhoneNumberContainer.style.display = '';
authNumberContainer.style.display = '';
changeMobilePhoneBtn.style.display = 'none';
});
}
// 기존 휴대폰 번호 가져오기 (하이픈 없이)
const originalPrefix = document.querySelector('[name="mobilePrefix"]').value;
const originalMiddle = document.querySelector('[name="mobileMiddle"]').value;
const originalLast = document.querySelector('[name="mobileLast"]').value;
const originalMobileNumber = `${originalPrefix}${originalMiddle}${originalLast}`;
// 초기화 실행
initializeMobileNumber();
});
</script>
</th:block>
// 새 번호에서도 하이픈 제거하여 비교
const newMobileRaw = newMobileNumber.replace(/-/g, '');
// 동일한 번호인지 체크
if (newMobileRaw === originalMobileNumber) {
customPopups.showAlert("동일한 휴대폰 번호입니다.");
return;
}
console.log('Original Mobile:', originalMobileNumber);
console.log('New Mobile:', newMobileNumber);
// jQuery AJAX 호출
$.ajax({
url: /*[[@{/auth/request_auth_number}]]*/ '/auth/request_auth_number',
type: 'POST',
data: {
mobileNumber: newMobileNumber,
_csrf: $('input[name="_csrf"]').val()
},
success: function (response) {
handleAuthNumberRequest(response);
},
error: function (xhr, status, error) {
customPopups.showAlert("처리 중 오류가 발생했습니다. 다시 시도해주세요.");
}
});
});
}
// 인증번호 확인 버튼 이벤트 리스너
let verifyAuthButton = document.querySelector(".btn_verify_auth");
if (verifyAuthButton) {
verifyAuthButton.addEventListener("click", function () {
let authNumber = document.getElementById('authNumber').value;
if (authNumber.length !== 6) {
customPopups.showAlert("6자리 인증번호를 입력해주세요.");
return;
}
let mobileNumber = document.getElementById('mobileNumber').value;
// jQuery AJAX 호출
$.ajax({
url: /*[[@{/auth/verify_auth_number}]]*/ '/auth/verify_auth_number',
type: 'POST',
data: {
mobileNumber: mobileNumber,
authNumber: authNumber,
_csrf: $('input[name="_csrf"]').val()
},
success: function (response) {
handleAuthNumberVerification(response);
},
error: function (xhr, status, error) {
customPopups.showAlert("처리 중 오류가 발생했습니다. 다시 시도해주세요.");
}
});
});
}
// 휴대폰 번호 변경 버튼 이벤트 리스너
const changeMobilePhoneBtn = document.querySelector('.change_mobile_phone');
const newPhoneNumberContainer = document.getElementById('newPhoneNumberContainer');
const authNumberContainer = document.getElementById('authNumberContainer');
if (changeMobilePhoneBtn && newPhoneNumberContainer && authNumberContainer) {
changeMobilePhoneBtn.addEventListener('click', function (e) {
e.preventDefault();
newPhoneNumberContainer.style.display = '';
authNumberContainer.style.display = '';
changeMobilePhoneBtn.style.display = 'none';
});
}
// 초기화 실행
initializeMobileNumber();
});
</script>
</th:block>
</body>
</html>
</html>
@@ -1,113 +1,123 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/djbank_title_layout}">
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/djbank_title_layout}">
<head>
<title>비밀번호 변경</title>
</head>
<body>
<section layout:fragment="title">
<div class="page-title-banner">
<img th:src="@{/img/img_title_bg.png}" class="title-image">
<h1>비밀번호 변경</h1>
</div>
</section>
<section layout:fragment="contentFragment">
<div class="service-main app-management-layout">
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('password')}"></th:block>
<div class="app-management-content">
<div class="password-change-wrapper">
<h2 class="page-outer-title">비밀번호 변경</h2>
<section layout:fragment="title">
<div class="page-title-banner">
<img th:src="@{/img/img_title_bg.png}" class="title-image">
<h1>비밀번호 변경</h1>
</div>
</section>
<section layout:fragment="contentFragment">
<div class="service-main app-management-layout">
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('password')}"></th:block>
<div class="app-management-content">
<div class="password-change-wrapper">
<h2 class="page-outer-title">비밀번호 변경</h2>
<form id="passwordChangeForm" th:action="@{/password/change}" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<form id="passwordChangeForm" th:action="@{/password/change}" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<div class="register-form-container">
<div class="info-notice-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#4685ef" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<path d="M12 16v-4"></path>
<path d="M12 8h.01"></path>
</svg>
<p>소중한 계정 보호를 위해 비밀번호를 변경해 주세요!</p>
<div class="register-form-container">
<div class="info-notice-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#4685ef"
stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<path d="M12 16v-4"></path>
<path d="M12 8h.01"></path>
</svg>
<p>소중한 계정 보호를 위해 비밀번호를 변경해 주세요!</p>
</div>
<div class="form-row">
<div class="form-label-wrapper">
<span class="form-label-text">새 비밀번호</span>
</div>
<div class="form-field-wrapper">
<input type="password" id="newPassword" name="newPassword" class="form-input"
placeholder="새 비밀번호 입력" required>
</div>
</div>
<div class="form-row">
<div class="form-label-wrapper">
<span class="form-label-text">새 비밀번호 확인</span>
</div>
<div class="form-field-wrapper">
<input type="password" name="confirmPassword" class="form-input"
placeholder="새 비밀번호 확인 입력" required>
</div>
</div>
<ul class="password-policy-checklist" data-password-input="newPassword">
<li data-rule="length" class="is-idle"><span class="policy-icon"></span><span
class="policy-text">영문/숫자/특수문자 포함 8~50자</span></li>
<li data-rule="letter" class="is-idle"><span class="policy-icon"></span><span
class="policy-text">영문 포함</span></li>
<li data-rule="digit" class="is-idle"><span class="policy-icon"></span><span
class="policy-text">숫자 포함</span></li>
<li data-rule="special" class="is-idle"><span class="policy-icon"></span><span
class="policy-text">특수문자 포함</span></li>
<li data-rule="nospace" class="is-idle"><span class="policy-icon"></span><span
class="policy-text">공백 사용 불가</span></li>
<li data-rule="norepeat" class="is-idle"><span class="policy-icon"></span><span
class="policy-text">동일 문자 3자리 이상 반복 불가</span></li>
<li data-rule="noseq" class="is-idle"><span class="policy-icon"></span><span
class="policy-text">연속된 문자/숫자 3자리 이상 불가</span></li>
</ul>
<p class="password-policy-note">※ 아이디, 휴대전화 번호는 비밀번호에 사용할 수 없습니다.</p>
</div>
<div class="form-row">
<div class="form-label-wrapper">
<span class="form-label-text">새 비밀번호</span>
</div>
<div class="form-field-wrapper">
<input type="password" id="newPassword" name="newPassword" class="form-input"
placeholder="새 비밀번호 입력" required>
<div class="form-actions" style="justify-content: flex-end;">
<div class="right-buttons">
<button type="button" class="btn-cancel" onclick="history.back()">취소</button>
<button type="submit" class="btn-apply btn-primary">변경</button>
</div>
</div>
<div class="form-row">
<div class="form-label-wrapper">
<span class="form-label-text">새 비밀번호 확인</span>
</div>
<div class="form-field-wrapper">
<input type="password" name="confirmPassword" class="form-input"
placeholder="새 비밀번호 확인 입력" required>
</div>
</div>
<ul class="password-policy-checklist" data-password-input="newPassword">
<li data-rule="length" class="is-idle"><span class="policy-icon"></span><span class="policy-text">영문/숫자/특수문자 포함 8~50자</span></li>
<li data-rule="letter" class="is-idle"><span class="policy-icon"></span><span class="policy-text">영문 포함</span></li>
<li data-rule="digit" class="is-idle"><span class="policy-icon"></span><span class="policy-text">숫자 포함</span></li>
<li data-rule="special" class="is-idle"><span class="policy-icon"></span><span class="policy-text">특수문자 포함</span></li>
<li data-rule="nospace" class="is-idle"><span class="policy-icon"></span><span class="policy-text">공백 사용 불가</span></li>
<li data-rule="norepeat" class="is-idle"><span class="policy-icon"></span><span class="policy-text">동일 문자 3자리 이상 반복 불가</span></li>
<li data-rule="noseq" class="is-idle"><span class="policy-icon"></span><span class="policy-text">연속된 문자/숫자 3자리 이상 불가</span></li>
</ul>
<p class="password-policy-note">아이디, 휴대전화 번호는 비밀번호에 사용할 수 없습니다.</p>
</div>
<div class="form-actions" style="justify-content: flex-end;">
<div class="right-buttons">
<button type="button" class="btn-cancel" onclick="history.back()">취소</button>
<button type="submit" class="btn-apply btn-primary">변경</button>
</div>
</div>
</form>
</form>
</div>
</div>
</div>
</div>
<script th:if="${error}" th:inline="javascript">
$(document).ready(function() {
customPopups.showAlert([[${error}]]);
})
</script>
<script th:inline="javascript">
// 반영 직전 2FA: twofaRequired 면 제출을 가로채 2FA 팝업 → 성공 시 실제 제출.
(function () {
var twofaRequired = /*[[${twofaRequired}]]*/ false;
if (!twofaRequired) return;
var form = document.getElementById('passwordChangeForm');
if (!form) return;
<script th:if="${error}" th:inline="javascript">
$(document).ready(function () {
customPopups.showAlert([[${ error }]]);
})
</script>
<script th:inline="javascript">
// 반영 직전 2FA: twofaRequired 면 제출을 가로채 2FA 팝업 → 성공 시 실제 제출.
(function () {
var twofaRequired = /*[[${twofaRequired}]]*/ false;
if (!twofaRequired) return;
var form = document.getElementById('passwordChangeForm');
if (!form) return;
form.addEventListener('submit', function (e) {
e.preventDefault();
form.addEventListener('submit', function (e) {
e.preventDefault();
var np = document.getElementById('newPassword').value;
var cp = form.querySelector('input[name="confirmPassword"]').value;
if (!np || !cp) { customPopups.showAlert('새 비밀번호를 입력해 주세요.'); return; }
if (np !== cp) { customPopups.showAlert('새 비밀번호가 일치하지 않습니다.'); return; }
if (window.PasswordPolicy && !PasswordPolicy.isValid(np)) {
customPopups.showAlert('비밀번호 규칙을 확인해 주세요.'); return;
}
var np = document.getElementById('newPassword').value;
var cp = form.querySelector('input[name="confirmPassword"]').value;
if (!np || !cp) { customPopups.showAlert('새 비밀번호를 입력해 주세요.'); return; }
if (np !== cp) { customPopups.showAlert('새 비밀번호가 일치하지 않습니다.'); return; }
if (window.PasswordPolicy && !PasswordPolicy.isValid(np)) {
customPopups.showAlert('비밀번호 규칙을 확인해 주세요.'); return;
}
if (typeof TwoFactorAuth === 'undefined') { form.submit(); return; }
TwoFactorAuth.open({
purpose: '/password/change',
// form.submit() 은 submit 이벤트를 재발생시키지 않으므로 그대로 서버로 전송된다.
onSuccess: function () { form.submit(); },
onCancel: function () { /* 사용자 취소 — 유지 */ }
});
});
})();
</script>
</section>
if (typeof TwoFactorAuth === 'undefined') { form.submit(); return; }
TwoFactorAuth.open({
purpose: '/password/change',
// form.submit() 은 submit 이벤트를 재발생시키지 않으므로 그대로 서버로 전송된다.
onSuccess: function () { form.submit(); },
onCancel: function () { /* 사용자 취소 — 유지 */ }
});
});
})();
</script>
</section>
</body>
</html>
</html>
@@ -10,33 +10,34 @@
</section>
<section layout:fragment="contentFragment">
<div class="org-register-container">
<div class="common-title-bar">
<h2 class="common-title">기본 정보</h2>
</div>
<div class="register-form-container">
<form id="updateForm" method="post" th:action="@{/mypage/update}">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<div class="register-form">
<div class="form-row">
<div class="form-label-wrapper label-offset">
<span class="form-label-text">소속기관</span>
</div>
<div class="form-field-wrapper">
<input type="text" th:value="${user.portalOrg.orgName}" name="organization"
class="form-input input-readonly info-value-box" placeholder="소속기관" disabled="disabled">
<div class="corp-manager-wrapper">
<div class="common-title-bar">
<h2 class="common-title">기본 정보</h2>
</div>
<div class="register-form-container">
<form id="updateForm" method="post" th:action="@{/mypage/update}">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<div class="register-form">
<div class="form-row">
<div class="form-label-wrapper">
<span class="form-label-text">소속기관</span>
</div>
<div class="form-field-wrapper">
<input type="text" th:value="${user.portalOrg.orgName}" name="organization"
class="form-input input-readonly info-value-box" placeholder="소속기관" disabled="disabled">
</div>
</div>
<th:block th:replace="~{apps/mypage/components/commonUserInfo :: commonUserInfo}"></th:block>
</div>
<th:block th:replace="~{apps/mypage/components/commonUserInfo :: commonUserInfo}"></th:block>
</form>
</div>
<!-- Action Buttons -->
<div class="form-actions form-actions--with-withdrawal">
<a class="withdrawal-link"><img th:src="@{/img/btn_withdrawal.png}" alt="회원탈퇴">회원탈퇴</a>
<div class="form-actions-buttons">
<button type="button" class="btn btn-submit btn-secondary" th:onclick="|location.href='@{/}'|">취소</button>
<button type="button" class="btn btn-submit btn-primary submit-btn">수정 적용</button>
</div>
</form>
</div>
<!-- Action Buttons -->
<div class="form-actions form-actions--with-withdrawal">
<a class="withdrawal-link"><img th:src="@{/img/btn_withdrawal.png}" alt="회원탈퇴">회원탈퇴</a>
<div class="form-actions-buttons">
<button type="button" class="btn btn-submit btn-secondary" th:onclick="|location.href='@{/}'|">취소</button>
<button type="button" class="btn btn-submit btn-primary submit-btn">수정 적용</button>
</div>
</div>
</div>
@@ -10,7 +10,7 @@
</section>
<section layout:fragment="contentFragment">
<div class="org-register-container">
<div class="corp-manager-wrapper">
<!-- 초대 알림 배너 -->
<div th:if="${hasPendingInvitation}" class="invitation_alert_banner" style="background-color: #f0f8ff; border: 1px solid #2196F3; border-radius: 4px; padding: 15px 20px; margin: 20px 0;">
<div style="display: flex; align-items: center; justify-content: space-between;">
@@ -27,32 +27,33 @@
</div>
<div class="common-title-bar">
<h2 class="common-title">기본 정보</h2>
</div>
<div class="register-form-container">
<form id="updateForm" method="post" th:action="@{/mypage/update}" th:object="${user}">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<div class="register-form">
<th:block th:replace="~{apps/mypage/components/commonUserInfo :: commonUserInfo}"></th:block>
</div>
<div class="common-title-bar">
<h2 class="common-title">기본 정보</h2>
</div>
<div class="register-form-container">
<form id="updateForm" method="post" th:action="@{/mypage/update}" th:object="${user}">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<div class="register-form">
<th:block th:replace="~{apps/mypage/components/commonUserInfo :: commonUserInfo}"></th:block>
</div>
<!-- Corporate Transfer Button -->
<div class="corporate-transfer-section">
<button type="button" class="btn btn-secondary btn_business btn-block">
법인회원 전환
</button>
</div>
</form>
</div>
<input type="hidden" name="finalMobileNumber"/>
<!-- Corporate Transfer Button -->
<div class="corporate-transfer-section">
<button type="button" class="btn btn-secondary btn_business btn-block">
법인회원 전환
</button>
</div>
</form>
</div>
<input type="hidden" name="finalMobileNumber"/>
<!-- Action Buttons -->
<div class="form-actions form-actions--with-withdrawal">
<a class="withdrawal-link"><img th:src="@{/img/btn_withdrawal.png}" alt="회원탈퇴">회원탈퇴</a>
<div class="form-actions-buttons">
<button type="button" class="btn btn-submit btn-secondary" th:onclick="|location.href='@{/}'|">취소</button>
<button type="button" class="btn btn-submit btn-primary submit-btn">수정</button>
<!-- Action Buttons -->
<div class="form-actions form-actions--with-withdrawal">
<a class="withdrawal-link"><img th:src="@{/img/btn_withdrawal.png}" alt="회원탈퇴">회원탈퇴</a>
<div class="form-actions-buttons">
<button type="button" class="btn btn-submit btn-secondary" th:onclick="|location.href='@{/}'|">취소</button>
<button type="button" class="btn btn-submit btn-primary submit-btn">수정</button>
</div>
</div>
</div>
</div>
@@ -1,64 +1,58 @@
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_base_layout}">
<body>
<th:block th:fragment="commonUserInfo">
<div class="org-form-group">
<label class="org-form-label">
이메일 아이디 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<div class="org-input-row">
<div class="org-compound-input">
<input type="text" id="userId" class="org-form-input"
th:classappend="${isInvited ? 'input-readonly' : ''}"
maxlength="30"
th:value="${emailId}"
th:readonly="${isInvited}"
th:placeholder="#{portalUser.Register.email}">
<span class="separator">@</span>
<input type="text" id="domain" class="org-form-input"
th:classappend="${isInvited ? 'input-readonly' : ''}"
maxlength="50"
th:value="${domain}"
th:readonly="${isInvited}"
th:placeholder="#{portalUser.Register.domain}">
</div>
<button type="button" class="btn org-btn-check btn_check_email"
th:classappend="${isInvited ? 'hidden' : ''}">
중복체크
</button>
</div>
<input type="hidden" id="loginId" name="loginId" th:value="${loginId}">
<input type="hidden" name="registrationType" th:value="${registrationType}"/>
<input type="hidden" id="registrationScenario" name="registrationScenario" value="new"/>
<div id="email-validation" class="org-validation-message"></div>
</div>
</div>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/kbank_base_layout}">
<!-- 이메일 인증 (중복체크 통과 후 노출) -->
<div class="org-form-group" id="emailVerifyRow" style="display: none;">
<label class="org-form-label">
이메일 인증 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<div class="org-input-row">
<button type="button" class="btn org-btn-check" id="btnSendEmailCode">인증번호 발송</button>
</div>
<div class="org-input-row" id="emailCodeRow" style="display: none; margin-top: 8px;">
<div class="org-compound-input" style="position: relative; flex: 1;">
<input type="text" id="emailAuthCode" class="org-form-input" maxlength="6"
inputmode="numeric" autocomplete="one-time-code" placeholder="인증번호 6자리">
<span class="org-timer" id="emailCertifyTime"
style="position: absolute; right: 12px; top: 50%; transform: translateY(-50%); color: #E11D48;">03:00</span>
<body>
<th:block th:fragment="commonUserInfo">
<div class="org-form-group">
<label class="org-form-label">
이메일 아이디 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<div class="org-input-row">
<div class="org-compound-input email-input-group">
<input type="text" id="userId" class="org-form-input"
th:classappend="${isInvited ? 'input-readonly' : ''}" maxlength="30" th:value="${emailId}"
th:readonly="${isInvited}" th:placeholder="#{portalUser.Register.email}">
<span class="separator">@</span>
<input type="text" id="domain" class="org-form-input"
th:classappend="${isInvited ? 'input-readonly' : ''}" maxlength="50" th:value="${domain}"
th:readonly="${isInvited}" th:placeholder="#{portalUser.Register.domain}">
</div>
<button type="button" class="btn-action-primary md" th:classappend="${isInvited ? 'hidden' : ''}">
중복체크
</button>
</div>
<button type="button" class="btn org-btn-check" id="btnVerifyEmailCode">인증확인</button>
<input type="hidden" id="loginId" name="loginId" th:value="${loginId}">
<input type="hidden" name="registrationType" th:value="${registrationType}" />
<input type="hidden" id="registrationScenario" name="registrationScenario" value="new" />
<div id="email-validation" class="org-validation-message"></div>
</div>
<input type="hidden" id="emailVerified" name="emailVerified" value="false"/>
<div id="email-auth-validation" class="org-validation-message"></div>
</div>
</div>
</th:block>
<!-- 이메일 인증 (중복체크 통과 후 노출) -->
<div class="org-form-group" id="emailVerifyRow" style="display: none;">
<label class="org-form-label">
이메일 인증 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<div class="org-input-row">
<button type="button" class="btn-action-primary md" id="btnSendEmailCode">인증번호 발송</button>
</div>
<div class="org-input-row" id="emailCodeRow" style="display: none; margin-top: 8px;">
<div class="org-compound-input" style="position: relative; flex: 1;">
<input type="text" id="emailAuthCode" class="org-form-input" maxlength="6" inputmode="numeric"
autocomplete="one-time-code" placeholder="인증번호 6자리">
<span class="org-timer" id="emailCertifyTime"
style="position: absolute; right: 12px; top: 50%; transform: translateY(-50%); color: #E11D48;">03:00</span>
</div>
<button type="button" class="btn-action-primary md" id="btnVerifyEmailCode">인증확인</button>
</div>
<input type="hidden" id="emailVerified" name="emailVerified" value="false" />
<div id="email-auth-validation" class="org-validation-message"></div>
</div>
</div>
</th:block>
</body>
<th:block>
<script th:fragment="commonUserInfoScript" th:inline="javascript">
@@ -195,7 +189,12 @@
function setEmailAuthMsg(msg, isError) {
var el = $('#email-auth-validation');
el.text(msg || '');
el.css('color', isError ? '#E11D48' : '#0049B4');
if (msg) {
el.removeClass('success error').addClass(isError ? 'error' : 'success');
el.css('display', 'block');
} else {
el.css('display', 'none');
}
}
function startEmailCodeTimer() {
@@ -284,4 +283,5 @@
</script>
</th:block>
</html>
</html>
@@ -1,111 +1,120 @@
<!DOCTYPE html>
<html lang="ko" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_base_layout}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_base_layout}">
<body>
<th:block th:fragment="newUserForm">
<div id="newUserFields">
<!-- 성명 -->
<div class="org-form-group">
<label class="org-form-label">
성명 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<input type="text" th:field="*{userName}" id="userName" class="org-form-input"
th:placeholder="#{portalUser.Register.userName}">
<div id="userName-validation" class="org-validation-message"></div>
</div>
</div>
<!-- 휴대폰 번호 입력 -->
<div class="org-form-group">
<label class="org-form-label">
휴대폰 번호 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<div class="org-input-row">
<div class="org-compound-input">
<select class="org-form-select phone-number" id="phoneMobile">
<option value="">선택</option>
<option value="010">010</option>
<option value="011">011</option>
<option value="017">017</option>
<option value="016">016</option>
<option value="018">018</option>
</select>
<span class="separator">-</span>
<input type="text" id="cellPhone1" maxlength="4" class="org-form-input phone-number"
th:placeholder="#{portal.Register.phone1}">
<span class="separator">-</span>
<input type="text" id="cellPhone2" maxlength="4" class="org-form-input phone-number"
th:placeholder="#{portal.Register.phone2}">
</div>
<button type="button" class="btn org-btn-check btn_auth">
인증번호 받기
</button>
<th:block th:fragment="newUserForm">
<div id="newUserFields">
<!-- 성명 -->
<div class="org-form-group">
<label class="org-form-label">
성명 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<input type="text" th:field="*{userName}" id="userName" class="org-form-input"
th:placeholder="#{portalUser.Register.userName}">
<div id="userName-validation" class="org-validation-message"></div>
</div>
<input type="hidden" th:field="*{mobileNumber}" id="mobileNumber">
<div id="mobile-validation" class="org-validation-message"></div>
</div>
</div>
<!-- 인증번호 입력 -->
<div class="org-form-group" id="authNumberContainer" style="display: none;">
<label class="org-form-label">
인증번호 입력 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<div class="org-input-row">
<div class="org-auth-input-group">
<input type="text" th:field="*{authNumber}" id="authNumber" maxlength="6" pattern="[0-9]*"
inputmode="numeric" class="org-form-input" th:placeholder="#{ncrdRegister.validate.authNumber}" required>
<span class="org-timer" id="certify_time">03:00</span>
<!-- 휴대폰 번호 입력 -->
<div class="org-form-group">
<label class="org-form-label">
휴대폰 번호 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<div class="org-input-row">
<div class="org-compound-input phone-input-group">
<select class="org-form-select phone-number" id="phoneMobile">
<option value="">선택</option>
<option value="010">010</option>
<option value="011">011</option>
<option value="017">017</option>
<option value="016">016</option>
<option value="018">018</option>
</select>
<span class="separator">-</span>
<input type="text" id="cellPhone1" maxlength="4" class="org-form-input phone-number"
th:placeholder="#{portal.Register.phone1}">
<span class="separator">-</span>
<input type="text" id="cellPhone2" maxlength="4" class="org-form-input phone-number"
th:placeholder="#{portal.Register.phone2}">
</div>
<button type="button" class="btn btn-action-primary md btn_auth">
인증번호 받기
</button>
</div>
<button type="button" class="btn org-btn-check btn_verify_auth">
인증번호 확인
</button>
<input type="hidden" th:field="*{mobileNumber}" id="mobileNumber">
<div id="mobile-validation" class="org-validation-message"></div>
</div>
<div id="authNumber-validation" class="org-validation-message"></div>
</div>
</div>
<!-- 비밀번호 -->
<div class="org-form-group">
<label class="org-form-label">
비밀번호 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<input type="password" name="password" id="password" class="org-form-input"
th:placeholder="#{portalUser.Register.pass}">
<input type="hidden" name="isPasswordValid" id="isPasswordValid"/>
<div id="password-validation" class="org-validation-message"></div>
<ul class="password-policy-checklist" data-password-input="password">
<li data-rule="length" class="is-idle"><span class="policy-icon"></span><span class="policy-text">영문/숫자/특수문자 포함 8~50자</span></li>
<li data-rule="letter" class="is-idle"><span class="policy-icon"></span><span class="policy-text">영문 포함</span></li>
<li data-rule="digit" class="is-idle"><span class="policy-icon"></span><span class="policy-text">숫자 포함</span></li>
<li data-rule="special" class="is-idle"><span class="policy-icon"></span><span class="policy-text">특수문자 포함</span></li>
<li data-rule="nospace" class="is-idle"><span class="policy-icon"></span><span class="policy-text">공백 사용 불가</span></li>
<li data-rule="norepeat" class="is-idle"><span class="policy-icon"></span><span class="policy-text">동일 문자 3자리 이상 반복 불가</span></li>
<li data-rule="noseq" class="is-idle"><span class="policy-icon"></span><span class="policy-text">연속된 문자/숫자 3자리 이상 불가</span></li>
</ul>
<p class="password-policy-note">아이디, 휴대전화 번호는 비밀번호에 사용할 수 없습니다.</p>
<!-- 인증번호 입력 -->
<div class="org-form-group" id="authNumberContainer" style="display: none;">
<label class="org-form-label">
인증번호 입력 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<div class="org-input-row">
<div class="org-auth-input-group">
<input type="text" th:field="*{authNumber}" id="authNumber" maxlength="6" pattern="[0-9]*"
inputmode="numeric" class="org-form-input" th:placeholder="#{ncrdRegister.validate.authNumber}"
required>
<span class="org-timer" id="certify_time">03:00</span>
</div>
<button type="button" class="btn org-btn-check btn_verify_auth">
인증번호 확인
</button>
</div>
<div id="authNumber-validation" class="org-validation-message"></div>
</div>
</div>
</div>
<!-- 비밀번호 확인 -->
<div class="org-form-group">
<label class="org-form-label">
비밀번호 확인 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<input type="password" name="password2" id="password2" class="org-form-input"
th:placeholder="#{portalUser.Register.passConfirm}">
<input type="hidden" name="isPasswordMatch" id="isPasswordMatch"/>
<div id="password-match-validation" class="org-validation-message"></div>
<!-- 비밀번호 -->
<div class="org-form-group">
<label class="org-form-label">
비밀번호 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<input type="password" name="password" id="password" class="org-form-input"
th:placeholder="#{portalUser.Register.pass}">
<input type="hidden" name="isPasswordValid" id="isPasswordValid" />
<div id="password-validation" class="org-validation-message"></div>
<ul class="password-policy-checklist" data-password-input="password">
<li data-rule="length" class="is-idle"><span class="policy-icon"></span><span class="policy-text">영문/숫자/특수문자
포함 8~50자</span></li>
<li data-rule="letter" class="is-idle"><span class="policy-icon"></span><span class="policy-text">영문
포함</span></li>
<li data-rule="digit" class="is-idle"><span class="policy-icon"></span><span class="policy-text">숫자
포함</span></li>
<li data-rule="special" class="is-idle"><span class="policy-icon"></span><span class="policy-text">특수문자
포함</span></li>
<li data-rule="nospace" class="is-idle"><span class="policy-icon"></span><span class="policy-text">공백 사용
불가</span></li>
<li data-rule="norepeat" class="is-idle"><span class="policy-icon"></span><span class="policy-text">동일 문자
3자리 이상 반복 불가</span></li>
<li data-rule="noseq" class="is-idle"><span class="policy-icon"></span><span class="policy-text">연속된 문자/숫자
3자리 이상 불가</span></li>
</ul>
<p class="password-policy-note">※ 아이디, 휴대전화 번호는 비밀번호에 사용할 수 없습니다.</p>
</div>
</div>
</div>
</div>
</th:block>
<!-- 비밀번호 확인 -->
<div class="org-form-group">
<label class="org-form-label">
비밀번호 확인 <span class="required-badge">필수</span>
</label>
<div class="org-form-input-wrapper">
<input type="password" name="password2" id="password2" class="org-form-input"
th:placeholder="#{portalUser.Register.passConfirm}">
<input type="hidden" name="isPasswordMatch" id="isPasswordMatch" />
<div id="password-match-validation" class="org-validation-message"></div>
</div>
</div>
</div>
</th:block>
</body>
<th:block>
<script th:fragment="newUserScript">
@@ -135,12 +144,12 @@
return formattedNumber;
}
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('DOMContentLoaded', function () {
let countdownInterval;
let isAuthVerified = false;
// 휴대폰 번호 중간, 끝자리 입력 제한 및 형식화
$('#cellPhone1').on('input', function() {
$('#cellPhone1').on('input', function () {
let value = $(this).val().replace(/[^0-9]/g, '');
if (value.length > 4) {
value = value.slice(0, 4);
@@ -149,7 +158,7 @@
combineMobileNumber();
});
$('#cellPhone2').on('input', function() {
$('#cellPhone2').on('input', function () {
let value = $(this).val().replace(/[^0-9]/g, '');
if (value.length > 4) {
value = value.slice(0, 4);
@@ -159,12 +168,12 @@
});
// select 변경 감지
$('#phoneMobile').on('change', function() {
$('#phoneMobile').on('change', function () {
combineMobileNumber();
});
// 인증번호 입력 제한
$('#authNumber').on('input', function() {
$('#authNumber').on('input', function () {
$(this).val($(this).val().replace(/[^0-9]/g, ''));
});
@@ -299,7 +308,7 @@
let authNumberInput = document.getElementById('authNumber');
if (authNumberInput) {
authNumberInput.addEventListener('input', function() {
authNumberInput.addEventListener('input', function () {
this.value = this.value.replace(/\D/g, '');
this.classList.toggle('is-invalid', this.value.length !== 6);
});
@@ -307,13 +316,13 @@
let userNameInput = document.getElementById('userName');
if (userNameInput) {
userNameInput.addEventListener('input', function() {
userNameInput.addEventListener('input', function () {
this.classList.toggle('is-invalid', this.value.trim().length === 0);
});
}
// 인증번호 요청 버튼 이벤트 리스너
$('.btn_auth').on('click', function() {
$('.btn_auth').on('click', function () {
let mobileNumber = combineMobileNumber();
if (!mobileNumber) {
customPopups.showAlert('휴대폰 번호를 올바르게 입력해주세요.');
@@ -330,17 +339,17 @@
purpose: 'signup',
_csrf: $('input[name="_csrf"]').val()
},
success: function(response) {
success: function (response) {
handleAuthNumberRequest(response);
},
error: function() {
error: function () {
customPopups.showAlert('처리 중 오류가 발생했습니다. 다시 시도해주세요.');
}
});
});
// 인증번호 확인 버튼 이벤트 리스너
$('.btn_verify_auth').on('click', function() {
$('.btn_verify_auth').on('click', function () {
let authNumber = $('#authNumber').val();
if (authNumber.length !== 6) {
customPopups.showAlert('6자리 인증번호를 입력해주세요.');
@@ -355,17 +364,17 @@
authNumber: authNumber,
_csrf: $('input[name="_csrf"]').val()
},
success: function(response) {
success: function (response) {
handleAuthNumberVerification(response);
},
error: function() {
error: function () {
customPopups.showAlert('처리 중 오류가 발생했습니다. 다시 시도해주세요.');
}
});
});
// 비밀번호 검증
$('#password').on('blur', function() {
$('#password').on('blur', function () {
let password = $(this).val();
if (!password) {
return;
@@ -380,21 +389,21 @@
loginId: $('#loginId').val(),
_csrf: $('input[name="_csrf"]').val()
},
success: function(response) {
success: function (response) {
let targetElement = $('#password-validation');
targetElement.removeClass('success error');
targetElement.addClass(response.valid ? 'success' : 'error');
targetElement.text(response.message);
$('#isPasswordValid').val(response.valid);
},
error: function() {
error: function () {
customPopups.showAlert('처리 중 오류가 발생했습니다. 다시 시도해주세요.');
}
});
});
// 비밀번호 확인 검증
$('#password2').on('blur', function() {
$('#password2').on('blur', function () {
let password2 = $(this).val();
if (!password2) {
return;
@@ -408,14 +417,14 @@
password2: password2,
_csrf: $('input[name="_csrf"]').val()
},
success: function(response) {
success: function (response) {
let targetElement = $('#password-match-validation');
targetElement.removeClass('success error');
targetElement.addClass(response.valid ? 'success' : 'error');
targetElement.text(response.message);
$('#isPasswordMatch').val(response.valid);
},
error: function() {
error: function () {
customPopups.showAlert("처리 중 오류가 발생했습니다. 다시 시도해주세요.");
}
});
@@ -423,4 +432,5 @@
});
</script>
</th:block>
</html>
</html>
@@ -24,45 +24,61 @@
<!-- Alert Container -->
<div id="alertContainer"></div>
<div class="org-section-header" style="margin-top: 60px;margin-bottom: 60px;">
<h3>법인 회원 가입 후 서비스 또는 API 이용을 하실 수 있습니다.</h3>
</div>
<!-- Agreement Section -->
<th:block th:replace="~{apps/register/userAgreementContent :: agreementContent(${termsOfUse}, ${privacyCollect})}"></th:block>
<form name="portalOrg" id="registerForm" role="form" th:action="@{/signup/portalOrg}"
th:object="${portalOrg}" method="post">
<!-- Corporate Basic Information Section -->
<div class="org-section-header org-section-header--agreement">
<h3>법인 기본 정보</h3>
<span class="required-badge">필수 입력</span>
</div>
<th:block th:replace="~{apps/register/components/orgBasicInfoForm :: orgInfoForm}"></th:block>
<!-- Corporate Admin Information Section -->
<div class="org-section-header org-section-header--agreement" style="margin-top: 48px;">
<h3>법인 관리자 정보</h3>
<span class="required-badge">필수 입력</span>
</div>
<!-- Registration Card Wrapper -->
<div class="register-card-wrapper">
<!-- Info Notice -->
<div class="org-info-notice">
<ul>
<li>법인관리자는 API 신청 및 개발자 초대 등 모든 권한을 보유합니다.</li>
</ul>
<div class="notice-icon-wrapper">
<svg class="notice-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
</div>
<p class="notice-text">법인 회원 가입 후 서비스 또는 API 이용을 하실 수 있습니다.</p>
</div>
<th:block th:if="${registrationType == 'corporate'}">
<th:block th:replace="~{apps/register/components/orgUserInfoForm :: orgUserInfoForm}"></th:block>
</th:block>
<!-- Agreement Section -->
<th:block th:replace="~{apps/register/userAgreementContent :: agreementContent(${termsOfUse}, ${privacyCollect})}"></th:block>
<!-- Action Buttons -->
<div class="org-action-buttons">
<button type="button" class="btn btn-secondary" id="cancelButton">취소</button>
<button type="button" class="btn btn-primary btn_register">가입신청</button>
</div>
</form>
<form name="portalOrg" id="registerForm" role="form" th:action="@{/signup/portalOrg}"
th:object="${portalOrg}" method="post">
<!-- Corporate Basic Information Section -->
<div class="org-section-header org-section-header--agreement">
<h3>법인 기본 정보</h3>
<span class="required-badge">필수 입력</span>
</div>
<th:block th:replace="~{apps/register/components/orgBasicInfoForm :: orgInfoForm}"></th:block>
<!-- Corporate Admin Information Section -->
<div class="org-section-header org-section-header--agreement" style="margin-top: 48px;">
<h3>법인 관리자 정보</h3>
<span class="required-badge">필수 입력</span>
</div>
<div class="org-info-notice">
<div class="notice-icon-wrapper">
<svg class="notice-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
</div>
<p class="notice-text">법인관리자는 API 신청 및 개발자 초대 등 모든 권한을 보유합니다.</p>
</div>
<th:block th:if="${registrationType == 'corporate'}">
<th:block th:replace="~{apps/register/components/orgUserInfoForm :: orgUserInfoForm}"></th:block>
</th:block>
<!-- Action Buttons -->
<div class="org-action-buttons">
<button type="button" class="btn btn-secondary" id="cancelButton">취소</button>
<button type="button" class="btn btn-primary btn_register">가입신청</button>
</div>
</form>
</div>
<!-- Loading Overlay -->
<div class="org-loading-overlay" id="loadingOverlay">
@@ -1,65 +1,80 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/djbank_base_layout}">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/djbank_base_layout}">
<body>
<th:block layout:fragment="contentFragment">
<div class="signup-selection-page">
<div class="signup-selection-container">
<div class="signup-selection-card">
<!-- Logo -->
<div class="signup-logo">
<img th:src="@{/img/logo/logo_box.png}" alt="DJBank">
</div>
<th:block layout:fragment="contentFragment">
<div class="signup-selection-page">
<div class="signup-selection-container">
<div class="signup-selection-card">
<!-- Header Title -->
<div class="signup-header">
<div class="signup-title-row">
<svg class="signup-title-icon" width="35" height="35" viewBox="0 0 24 24" fill="currentColor"
xmlns="http://www.w3.org/2000/svg">
<path
d="M9 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4zm11-4h-2v2h-2v2h2v2h2v-2h2v-2h-2v-2z" />
</svg>
<h2 class="signup-title">회원가입</h2>
</div>
<p class="signup-message">DJBank API Portal 사용을 위해 회원 가입해 주세요.</p>
</div>
<!-- Message -->
<p class="signup-message">DJBank API Portal 사용을 위해 회원 가입해 주세요.</p>
<!-- Signup Cards -->
<div class="signup-cards">
<!-- Individual Signup Card -->
<a th:href="@{/signup/portalUser}" class="signup-card-item individual-card">
<div class="card-icon-wrapper">
<svg width="36" height="36" viewBox="0 0 22 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M15.4287 12.748C19.0578 12.7481 21.9998 15.7302 22 19.4092V29.3447C21.9998 29.6617 21.7783 29.9263 21.4834 29.9873L21.3535 30H0.646484C0.289425 29.9998 0.00024463 29.7067 0 29.3447V18.2607C0 15.2164 2.43446 12.748 5.4375 12.748C6.67211 12.7481 7.86991 13.1742 8.83398 13.9561L9.13965 14.2031C9.4187 14.4294 9.46443 14.8431 9.24121 15.126C9.01802 15.4088 8.61011 15.4546 8.33105 15.2285L8.02539 14.9805C7.29081 14.3848 6.37819 14.0596 5.4375 14.0596C3.1492 14.0596 1.29395 15.9409 1.29395 18.2607V28.6885H20.7061V19.4092C20.7058 16.4548 18.3431 14.0596 15.4287 14.0596C13.2292 14.0598 11.2606 15.4434 10.4883 17.5312L9.98828 18.8818C9.86275 19.2209 9.48978 19.3927 9.15527 19.2656C8.82079 19.1383 8.65091 18.7601 8.77637 18.4209L9.27637 17.0703C10.2382 14.4705 12.6898 12.7482 15.4287 12.748ZM11 0C14.2162 8.65396e-06 16.8231 2.64285 16.8232 5.90332C16.8232 9.16394 14.2163 11.8076 11 11.8076C7.78365 11.8076 5.17676 9.16394 5.17676 5.90332C5.17693 2.64284 7.78375 0 11 0ZM11 1.31152C8.4985 1.31152 6.47087 3.36743 6.4707 5.90332C6.4707 8.43936 8.49839 10.4951 11 10.4951C13.5016 10.4951 15.5293 8.43935 15.5293 5.90332C15.5291 3.36743 13.5015 1.31153 11 1.31152Z"
fill="currentColor" />
</svg>
</div>
<div class="card-text-wrapper">
<h3 class="card-title">개인 회원가입</h3>
<p class="card-desc">개인 개발자 및 API 연동 테스트를 원하시는 사용자를 위한 회원가입입니다.</p>
</div>
</a>
<!-- Signup Buttons -->
<div class="signup-buttons">
<!-- Individual Signup Button -->
<a th:href="@{/signup/portalUser}" class="signup-btn signup-btn-individual">
<svg width="22" height="30" viewBox="0 0 22 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M15.4287 12.748C19.0578 12.7481 21.9998 15.7302 22 19.4092V29.3447C21.9998 29.6617 21.7783 29.9263 21.4834 29.9873L21.3535 30H0.646484C0.289425 29.9998 0.00024463 29.7067 0 29.3447V18.2607C0 15.2164 2.43446 12.748 5.4375 12.748C6.67211 12.7481 7.86991 13.1742 8.83398 13.9561L9.13965 14.2031C9.4187 14.4294 9.46443 14.8431 9.24121 15.126C9.01802 15.4088 8.61011 15.4546 8.33105 15.2285L8.02539 14.9805C7.29081 14.3848 6.37819 14.0596 5.4375 14.0596C3.1492 14.0596 1.29395 15.9409 1.29395 18.2607V28.6885H20.7061V19.4092C20.7058 16.4548 18.3431 14.0596 15.4287 14.0596C13.2292 14.0598 11.2606 15.4434 10.4883 17.5312L9.98828 18.8818C9.86275 19.2209 9.48978 19.3927 9.15527 19.2656C8.82079 19.1383 8.65091 18.7601 8.77637 18.4209L9.27637 17.0703C10.2382 14.4705 12.6898 12.7482 15.4287 12.748ZM11 0C14.2162 8.65396e-06 16.8231 2.64285 16.8232 5.90332C16.8232 9.16394 14.2163 11.8076 11 11.8076C7.78365 11.8076 5.17676 9.16394 5.17676 5.90332C5.17693 2.64284 7.78375 0 11 0ZM11 1.31152C8.4985 1.31152 6.47087 3.36743 6.4707 5.90332C6.4707 8.43936 8.49839 10.4951 11 10.4951C13.5016 10.4951 15.5293 8.43935 15.5293 5.90332C15.5291 3.36743 13.5015 1.31153 11 1.31152Z"
fill="white"/>
</svg>
<span>개인회원 가입</span>
</a>
<!-- Organization Signup Button -->
<a th:href="@{/signup/portalOrg}" class="signup-btn signup-btn-organization">
<svg width="26" height="30" viewBox="0 0 26 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.001 7.2002C14.3062 7.20046 15.3652 8.27529 15.3652 9.60059V27.5996L15.3623 27.7236C15.3009 28.9505 14.3313 29.9346 13.123 29.9971L13.001 30H2.36426L2.24316 29.9971C1.03469 29.9348 0.0653104 28.9506 0.00390625 27.7236L0 27.5996V9.60059C1.90336e-07 8.27527 1.05901 7.20043 2.36426 7.2002H13.001ZM23.0469 0C24.6785 0.00011158 26.0009 1.3433 26.001 3V27C26.0009 28.6567 24.6785 29.9999 23.0469 30H16.5469C16.2205 30 15.9551 29.7308 15.9551 29.3994C15.9553 29.0682 16.2206 28.7998 16.5469 28.7998H23.0469C24.0258 28.7997 24.8192 27.9939 24.8193 27V3C24.8193 2.00604 24.0258 1.20031 23.0469 1.2002H13.5918C12.6128 1.20022 11.8194 2.00598 11.8193 3V6C11.8193 6.33124 11.5547 6.5994 11.2285 6.59961C10.9022 6.59961 10.6377 6.33137 10.6377 6V3C10.6378 1.34325 11.9601 2.44626e-05 13.5918 0H23.0469ZM2.36426 8.40039C1.71173 8.40062 1.18262 8.938 1.18262 9.60059V27.5996C1.18265 28.2622 1.71175 28.7996 2.36426 28.7998H4.1377V25.2002C4.1377 23.5435 5.46013 22.2004 7.0918 22.2002H8.86523C10.4966 22.2007 11.8193 23.5437 11.8193 25.2002V28.7998H13.001C13.6535 28.7995 14.1836 28.2621 14.1836 27.5996V9.60059C14.1836 8.93802 13.6535 8.40066 13.001 8.40039H2.36426ZM7.0918 23.4004C6.11285 23.4005 5.31934 24.2062 5.31934 25.2002V28.7998H10.6377V25.2002C10.6377 24.2064 9.84392 23.4009 8.86523 23.4004H7.0918ZM18.3193 24.5996H17.1377V22.2002H18.3193V24.5996ZM21.8652 24.5996H20.6836V22.2002H21.8652V24.5996ZM18.3193 20.4004H17.1377V18H18.3193V20.4004ZM21.8652 20.4004H20.6836V18H21.8652V20.4004ZM4.72852 18.5996H3.54688V16.2002H4.72852V18.5996ZM8.27441 18.5996H7.09277V16.2002H8.27441V18.5996ZM11.8193 18.5996H10.6377V16.2002H11.8193V18.5996ZM18.3193 16.2002H17.1377V13.7998H18.3193V16.2002ZM21.8652 16.2002H20.6836V13.7998H21.8652V16.2002ZM4.72852 14.4004H3.54688V12H4.72852V14.4004ZM8.27441 14.4004H7.09277V12H8.27441V14.4004ZM11.8193 14.4004H10.6377V12H11.8193V14.4004ZM18.3193 12H17.1377V9.60059H18.3193V12ZM21.8652 12H20.6836V9.60059H21.8652V12ZM18.3193 7.7998H17.1377V5.40039H18.3193V7.7998ZM21.8652 7.7998H20.6836V5.40039H21.8652V7.7998Z"
fill="white"/>
</svg>
<span>법인회원 가입</span>
</a>
</div>
<!-- Navigation Links -->
<div class="signup-navigation">
<a th:href="@{/}" class="signup-nav-link">홈으로</a>
<span class="signup-nav-separator">|</span>
<a th:href="@{/login}" class="signup-nav-link">로그인 하기</a>
<!-- Organization Signup Card -->
<a th:href="@{/signup/portalOrg}" class="signup-card-item organization-card">
<div class="card-icon-wrapper">
<svg width="36" height="36" viewBox="0 0 26 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.001 7.2002C14.3062 7.20046 15.3652 8.27529 15.3652 9.60059V27.5996L15.3623 27.7236C15.3009 28.9505 14.3313 29.9346 13.123 29.9971L13.001 30H2.36426L2.24316 29.9971C1.03469 29.9348 0.0653104 28.9506 0.00390625 27.7236L0 27.5996V9.60059C1.90336e-07 8.27527 1.05901 7.20043 2.36426 7.2002H13.001ZM23.0469 0C24.6785 0.00011158 26.0009 1.3433 26.001 3V27C26.0009 28.6567 24.6785 29.9999 23.0469 30H16.5469C16.2205 30 15.9551 29.7308 15.9551 29.3994C15.9553 29.0682 16.2206 28.7998 16.5469 28.7998H23.0469C24.0258 28.7997 24.8192 27.9939 24.8193 27V3C24.8193 2.00604 24.0258 1.20031 23.0469 1.2002H13.5918C12.6128 1.20022 11.8194 2.00598 11.8193 3V6C11.8193 6.33124 11.5547 6.5994 11.2285 6.59961C10.9022 6.59961 10.6377 6.33137 10.6377 6V3C10.6378 1.34325 11.9601 2.44626e-05 13.5918 0H23.0469ZM2.36426 8.40039C1.71173 8.40062 1.18262 8.938 1.18262 9.60059V27.5996C1.18265 28.2622 1.71175 28.7996 2.36426 28.7998H4.1377V25.2002C4.1377 23.5435 5.46013 22.2004 7.0918 22.2002H8.86523C10.4966 22.2007 11.8193 23.5437 11.8193 25.2002V28.7998H13.001C13.6535 28.7995 14.1836 28.2621 14.1836 27.5996V9.60059C14.1836 8.93802 13.6535 8.40066 13.001 8.40039H2.36426ZM7.0918 23.4004C6.11285 23.4005 5.31934 24.2062 5.31934 25.2002V28.7998H10.6377V25.2002C10.6377 24.2064 9.84392 23.4009 8.86523 23.4004H7.0918ZM18.3193 24.5996H17.1377V22.2002H18.3193V24.5996ZM21.8652 24.5996H20.6836V22.2002H21.8652V24.5996ZM18.3193 20.4004H17.1377V18H18.3193V20.4004ZM21.8652 20.4004H20.6836V18H21.8652V20.4004ZM4.72852 18.5996H3.54688V16.2002H4.72852V18.5996ZM8.27441 18.5996H7.09277V16.2002H8.27441V18.5996ZM11.8193 18.5996H10.6377V16.2002H11.8193V18.5996ZM18.3193 16.2002H17.1377V13.7998H18.3193V16.2002ZM21.8652 16.2002H20.6836V13.7998H21.8652V16.2002ZM4.72852 14.4004H3.54688V12H4.72852V14.4004ZM8.27441 14.4004H7.09277V12H8.27441V14.4004ZM11.8193 14.4004H10.6377V12H11.8193V14.4004ZM18.3193 12H17.1377V9.60059H18.3193V12ZM21.8652 12H20.6836V9.60059H21.8652V12ZM18.3193 7.7998H17.1377V5.40039H18.3193V7.7998ZM21.8652 7.7998H20.6836V5.40039H21.8652V7.7998Z"
fill="currentColor" />
</svg>
</div>
<div class="card-text-wrapper">
<h3 class="card-title">법인 회원가입</h3>
<p class="card-desc">기업 및 기관 파트너로서 정식 API 서비스 연동 및 운영을 위한 회원가입입니다.</p>
</div>
</a>
</div>
<!-- Navigation Links -->
<div class="signup-navigation-box">
<a th:href="@{/}" class="signup-nav-btn">홈으로</a>
<div class="signup-nav-divider"></div>
<a th:href="@{/login}" class="signup-nav-btn">로그인 하기</a>
</div>
</div>
</div>
</div>
</div>
</th:block>
<th:block layout:fragment="script">
<script th:src="@{/js/jquery-3.7.1.min.js}"></script>
<script th:src="@{/js/daterangepicker.js}"></script>
<script th:src="@{/js/slick.min.js}"></script>
<script th:src="@{/js/front2.js}"></script>
</th:block>
<th:block layout:fragment="contentScript">
<script>
// Modern signup selection page - no custom JS needed
// Card interactions handled by CSS and standard anchor tags
</script>
</th:block>
</th:block>
<th:block layout:fragment="script">
<script th:src="@{/js/jquery-3.7.1.min.js}"></script>
<script th:src="@{/js/daterangepicker.js}"></script>
<script th:src="@{/js/slick.min.js}"></script>
<script th:src="@{/js/front2.js}"></script>
</th:block>
<th:block layout:fragment="contentScript">
<script>
// Modern signup selection page - no custom JS needed
// Card interactions handled by CSS and standard anchor tags
</script>
</th:block>
</body>
</html>
</html>
@@ -1,166 +1,291 @@
<!DOCTYPE html>
<html lang="ko" xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/base_layout}">
<body>
<th:block th:fragment="agreementContent(termsOfUse, privacyCollect)">
<!-- Agreement Section Header -->
<div class="org-section-header org-section-header--agreement">
<h3 th:text="${agreementTitle} ?: '약관 동의'">약관 동의</h3>
<span class="required-badge">필수 동의</span>
</div>
<html lang="ko" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/base_layout}">
<form id="agreementForm" class="agreement-form">
<!-- Agree All Checkbox -->
<div class="agreement-all-section">
<label class="agreement-checkbox-label">
<input type="checkbox" id="agree_all" class="agreement-checkbox-input">
<span class="agreement-checkbox-custom"></span>
<span class="agreement-checkbox-text agreement-all-text">전체동의</span>
</label>
<body>
<th:block th:fragment="agreementContent(termsOfUse, privacyCollect)">
<!-- Agreement Section Header -->
<div class="org-section-header org-section-header--agreement">
<h3 th:text="${agreementTitle} ?: '약관 동의'">약관 동의</h3>
<span class="required-badge">필수 동의</span>
</div>
<!-- Divider -->
<div class="agreement-divider"></div>
<!-- Individual Agreements -->
<div class="agreement-items-list">
<!-- Terms of Use -->
<div class="agreement-item-row">
<form id="agreementForm" class="agreement-form">
<!-- Agree All Checkbox -->
<div class="agreement-all-section">
<label class="agreement-checkbox-label">
<input type="checkbox" name="termsOfUse" id="termsOfUse" class="agreement-checkbox-input" required>
<input type="checkbox" id="agree_all" class="agreement-checkbox-input">
<span class="agreement-checkbox-custom"></span>
<span class="agreement-checkbox-text">
<span class="agreement-required">[필수]</span>
<span class="agreement-title">이용약관 동의</span>
</span>
<span class="agreement-checkbox-text agreement-all-text">전체동의</span>
</label>
<button type="button" class="agreement-toggle-icon" data-target="termsOfUseContent">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<div id="termsOfUseContent" class="agreement-content" style="display:none;">
<div class="agreement-scroll">
<div class="editor-content" th:utext="${termsOfUse?.contents ?: '이용약관 내용을 불러올 수 없습니다.'}"></div>
</div>
</div>
<!-- Privacy Policy -->
<div class="agreement-item-row">
<label class="agreement-checkbox-label">
<input type="checkbox" name="privacyCollect" id="privacyCollect" class="agreement-checkbox-input" required>
<span class="agreement-checkbox-custom"></span>
<th:block th:switch="${registrationType}">
<span class="agreement-checkbox-text" th:case="'personal'">
<span class="agreement-required">[필수]</span>
<span class="agreement-title">개인정보 수집 및 이용 동의</span>
</span>
<span class="agreement-checkbox-text" th:case="'corporate'">
<span class="agreement-required">[필수]</span>
<span class="agreement-title">개인정보 수집 및 이용 동의</span>
</span>
</th:block>
</label>
<button type="button" class="agreement-toggle-icon" data-target="privacyCollectContent">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<div id="privacyCollectContent" class="agreement-content" style="display:none;">
<div class="agreement-scroll">
<div class="editor-content" th:utext="${privacyCollect?.contents ?: '개인정보수집동의서 내용을 불러올 수 없습니다.'}"></div>
</div>
</div>
<!-- Divider -->
<div class="agreement-divider"></div>
<!-- Notification Consent (모델에 notificationConsent가 있을 때만 노출) -->
<th:block th:if="${notificationConsent != null}">
<!-- Individual Agreements -->
<div class="agreement-items-list">
<!-- Terms of Use -->
<div class="agreement-item-row">
<label class="agreement-checkbox-label">
<input type="checkbox" name="notificationConsent" id="notificationConsent" class="agreement-checkbox-input" required>
<input type="checkbox" name="termsOfUse" id="termsOfUse" class="agreement-checkbox-input"
required disabled>
<span class="agreement-checkbox-custom"></span>
<span class="agreement-checkbox-text">
<span class="agreement-required">[필수]</span>
<span class="agreement-title">알림 수신 동의</span>
<span class="agreement-title">이용약관 동의</span>
</span>
</label>
<button type="button" class="agreement-toggle-icon" data-target="notificationConsentContent">
<button type="button" class="agreement-toggle-icon" data-target="termsOfUseContent">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<div id="notificationConsentContent" class="agreement-content" style="display:none;">
<div class="agreement-scroll">
<div class="editor-content" th:utext="${notificationConsent?.contents ?: '알림 수신 동의서 내용을 불러올 수 없습니다.'}"></div>
<div id="termsOfUseContent" class="agreement-content" style="display:none;">
<div class="agreement-scroll" style="max-height: 400px; overflow-y: auto;">
<div class="editor-content" th:utext="${termsOfUse?.contents ?: '이용약관 내용을 불러올 수 없습니다.'}"></div>
</div>
<div class="agreement-btn-wrapper" style="text-align: center; margin-top: 15px;">
<button type="button" class="btn-action-primary md agreement-agree-btn"
data-checkbox-id="termsOfUse" disabled>동의</button>
</div>
</div>
</th:block>
</div>
</form>
</th:block>
<script th:fragment="agreementScript">
document.addEventListener('DOMContentLoaded', function () {
const agreeAll = document.getElementById('agree_all');
const agreementChecks = document.querySelectorAll('.agreement-checkbox-input[required]');
const toggleIcons = document.querySelectorAll('.agreement-toggle-icon');
<!-- Privacy Policy -->
<div class="agreement-item-row">
<label class="agreement-checkbox-label">
<input type="checkbox" name="privacyCollect" id="privacyCollect"
class="agreement-checkbox-input" required disabled>
<span class="agreement-checkbox-custom"></span>
<th:block th:switch="${registrationType}">
<span class="agreement-checkbox-text" th:case="'personal'">
<span class="agreement-required">[필수]</span>
<span class="agreement-title">개인정보 수집 및 이용 동의</span>
</span>
<span class="agreement-checkbox-text" th:case="'corporate'">
<span class="agreement-required">[필수]</span>
<span class="agreement-title">개인정보 수집 및 이용 동의</span>
</span>
</th:block>
</label>
<button type="button" class="agreement-toggle-icon" data-target="privacyCollectContent">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<div id="privacyCollectContent" class="agreement-content" style="display:none;">
<div class="agreement-scroll" style="max-height: 400px; overflow-y: auto;">
<div class="editor-content"
th:utext="${privacyCollect?.contents ?: '개인정보수집동의서 내용을 불러올 수 없습니다.'}"></div>
</div>
<div class="agreement-btn-wrapper" style="text-align: center; margin-top: 15px;">
<button type="button" class="btn-action-primary md agreement-agree-btn"
data-checkbox-id="privacyCollect" disabled>동의</button>
</div>
</div>
// 전체 동의 체크박스 이벤트
if (agreeAll) {
agreeAll.addEventListener('change', function () {
agreementChecks.forEach(check => {
check.checked = this.checked;
});
});
}
<!-- Notification Consent (모델에 notificationConsent가 있을 때만 노출) -->
<th:block th:if="${notificationConsent != null}">
<div class="agreement-item-row">
<label class="agreement-checkbox-label">
<input type="checkbox" name="notificationConsent" id="notificationConsent"
class="agreement-checkbox-input" required disabled>
<span class="agreement-checkbox-custom"></span>
<span class="agreement-checkbox-text">
<span class="agreement-required">[필수]</span>
<span class="agreement-title">알림 수신 동의</span>
</span>
</label>
<button type="button" class="agreement-toggle-icon" data-target="notificationConsentContent">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<div id="notificationConsentContent" class="agreement-content" style="display:none;">
<div class="agreement-scroll" style="max-height: 400px; overflow-y: auto;">
<div class="editor-content"
th:utext="${notificationConsent?.contents ?: '알림 수신 동의서 내용을 불러올 수 없습니다.'}"></div>
</div>
<div class="agreement-btn-wrapper" style="text-align: center; margin-top: 15px;">
<button type="button" class="btn-action-primary md agreement-agree-btn"
data-checkbox-id="notificationConsent" disabled>동의</button>
</div>
</div>
</th:block>
</div>
</form>
</th:block>
// 개별 체크박스 이벤트
agreementChecks.forEach(check => {
check.addEventListener('change', function () {
if (agreeAll) {
agreeAll.checked = Array.from(agreementChecks).every(c => c.checked);
}
});
});
<script th:fragment="agreementScript">
document.addEventListener('DOMContentLoaded', function () {
const agreeAll = document.getElementById('agree_all');
const agreementChecks = document.querySelectorAll('.agreement-checkbox-input[required]');
const toggleIcons = document.querySelectorAll('.agreement-toggle-icon');
// 토글 아이콘 이벤트
toggleIcons.forEach(icon => {
icon.addEventListener('click', function (e) {
e.preventDefault();
const targetId = this.getAttribute('data-target');
const targetContent = document.getElementById(targetId);
const parentRow = this.closest('.agreement-item-row');
// 현재 섹션이 이미 열려있는지 확인
const isCurrentlyOpen = targetContent.style.display === 'block';
// 모든 섹션 닫기
document.querySelectorAll('.agreement-content').forEach(content => {
content.style.display = 'none';
});
document.querySelectorAll('.agreement-toggle-icon').forEach(button => {
button.classList.remove('active');
});
document.querySelectorAll('.agreement-item-row').forEach(item => {
item.classList.remove('active');
});
// 현재 섹션이 닫혀있었다면 열기
if (!isCurrentlyOpen) {
targetContent.style.display = 'block';
this.classList.add('active');
if (parentRow) {
parentRow.classList.add('active');
// 전체동의 클릭 시 아직 다 읽지 않은 약관이 있으면 경고 알림 표시
if (agreeAll) {
agreeAll.addEventListener('click', function (e) {
const hasLocked = Array.from(agreementChecks).some(check => check.disabled);
if (hasLocked) {
e.preventDefault();
this.checked = false;
customPopups.showAlert('모든 약관을 상세히 펼쳐서 끝까지 스크롤하여 읽으신 후에 동의하실 수 있습니다.');
}
});
setTimeout(() => {
targetContent.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}, 100);
}
agreeAll.addEventListener('change', function () {
agreementChecks.forEach(check => {
if (!check.disabled) {
check.checked = this.checked;
}
});
});
}
// 개별 체크박스 이벤트
agreementChecks.forEach(check => {
check.addEventListener('change', function () {
if (agreeAll) {
agreeAll.checked = Array.from(agreementChecks).every(c => c.checked);
}
});
});
// 스크롤 이벤트 감지하여 동의 활성화
document.querySelectorAll('.agreement-scroll').forEach(scrollContainer => {
scrollContainer.addEventListener('scroll', function () {
// 스크롤 끝 감지 (여유 마진 10px)
if (this.scrollTop + this.clientHeight >= this.scrollHeight - 10) {
const contentDiv = this.closest('.agreement-content');
if (contentDiv) {
const targetId = contentDiv.id;
const checkboxId = targetId.replace('Content', '');
const checkbox = document.getElementById(checkboxId);
if (checkbox && checkbox.disabled) {
checkbox.disabled = false;
// 모든 필수 동의가 활성화되었는지 확인 후 전체동의 해제
if (agreeAll) {
const allEnabled = Array.from(agreementChecks).every(c => !c.disabled);
if (allEnabled) {
agreeAll.disabled = false;
}
}
}
// 동의 버튼 활성화
const agreeBtn = contentDiv.querySelector('.agreement-agree-btn');
if (agreeBtn) {
agreeBtn.disabled = false;
}
}
}
});
});
// 동의 버튼 클릭 시 동작
document.querySelectorAll('.agreement-agree-btn').forEach(btn => {
btn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
const checkboxId = this.getAttribute('data-checkbox-id');
const checkbox = document.getElementById(checkboxId);
if (checkbox) {
checkbox.checked = true;
checkbox.dispatchEvent(new Event('change'));
}
// 해당 토글 닫기
const contentDiv = this.closest('.agreement-content');
if (contentDiv) {
contentDiv.style.display = 'none';
// 토글 버튼 active 제거
const targetId = contentDiv.id;
const toggleBtn = document.querySelector(`.agreement-toggle-icon[data-target="${targetId}"]`);
if (toggleBtn) {
toggleBtn.classList.remove('active');
}
const parentRow = contentDiv.previousElementSibling;
if (parentRow && parentRow.classList.contains('agreement-item-row')) {
parentRow.classList.remove('active');
}
}
});
});
// 토글 아이콘 이벤트
toggleIcons.forEach(icon => {
icon.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
const targetId = this.getAttribute('data-target');
const targetContent = document.getElementById(targetId);
const parentRow = this.closest('.agreement-item-row');
// 현재 섹션이 이미 열려있는지 확인
const isCurrentlyOpen = targetContent.style.display === 'block';
// 모든 섹션 닫기
document.querySelectorAll('.agreement-content').forEach(content => {
content.style.display = 'none';
});
document.querySelectorAll('.agreement-toggle-icon').forEach(button => {
button.classList.remove('active');
});
document.querySelectorAll('.agreement-item-row').forEach(item => {
item.classList.remove('active');
});
// 현재 섹션이 닫혀있었다면 열기
if (!isCurrentlyOpen) {
targetContent.style.display = 'block';
this.classList.add('active');
if (parentRow) {
parentRow.classList.add('active');
}
// 스크롤 공간이 없거나 매우 짧은 글의 경우 즉시 체크박스 및 동의 버튼 활성화
const scrollContainer = targetContent.querySelector('.agreement-scroll');
if (scrollContainer) {
if (scrollContainer.scrollHeight <= scrollContainer.clientHeight) {
const checkboxId = targetId.replace('Content', '');
const checkbox = document.getElementById(checkboxId);
if (checkbox && checkbox.disabled) {
checkbox.disabled = false;
if (agreeAll) {
const allEnabled = Array.from(agreementChecks).every(c => !c.disabled);
if (allEnabled) {
agreeAll.disabled = false;
}
}
}
const agreeBtn = targetContent.querySelector('.agreement-agree-btn');
if (agreeBtn) {
agreeBtn.disabled = false;
}
}
}
setTimeout(() => {
targetContent.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}, 100);
}
});
});
// 행 한줄 클릭시 토글 동작 (체크박스 클릭 제외)
document.querySelectorAll('.agreement-item-row').forEach(row => {
row.style.cursor = 'pointer';
row.addEventListener('click', function (e) {
if (e.target.closest('.agreement-checkbox-input') || e.target.closest('.agreement-checkbox-custom')) {
return;
}
const toggleIcon = this.querySelector('.agreement-toggle-icon');
if (toggleIcon) {
toggleIcon.click();
}
});
});
});
});
</script>
</script>
</body>
</html>
@@ -20,50 +20,58 @@
<!-- Alert Container -->
<div id="alertContainer"></div>
<!-- Info Notice -->
<div class="org-info-notice" th:if="${!isInvited}">
<ul>
<li>서비스 또는 API 사용을 원하실 경우 법인회원 승인 후 이용하실 수 있습니다.</li>
</ul>
</div>
<!-- Agreement Section -->
<th:block th:replace="~{apps/register/userAgreementContent :: agreementContent(${termsOfUse}, ${privacyCollect})}"></th:block>
<form name="portalUser" id="registerForm" role="form" th:action="@{/signup/portalUser}"
th:object="${portalUser}" method="post">
<!-- Corporate Information Section (invited users only) -->
<div th:if="${isInvited}">
<div class="org-section-header org-section-header--agreement">
<h3>법인 정보</h3>
<!-- Registration Card Wrapper -->
<div class="register-card-wrapper">
<!-- Info Notice -->
<div class="org-info-notice" th:if="${!isInvited}">
<div class="notice-icon-wrapper">
<svg class="notice-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
</div>
<p class="notice-text">서비스 또는 API 사용을 원하실 경우 법인회원 승인 후 이용하실 수 있습니다.</p>
</div>
<div class="org-form-group">
<label class="org-form-label">법인명</label>
<div class="org-form-input-wrapper">
<input type="text" name="companyName" class="org-form-input" th:value="${orgName}" readonly>
<!-- Agreement Section -->
<th:block th:replace="~{apps/register/userAgreementContent :: agreementContent(${termsOfUse}, ${privacyCollect})}"></th:block>
<form name="portalUser" id="registerForm" role="form" th:action="@{/signup/portalUser}"
th:object="${portalUser}" method="post">
<!-- Corporate Information Section (invited users only) -->
<div th:if="${isInvited}">
<div class="org-section-header org-section-header--agreement">
<h3>법인 정보</h3>
</div>
<div class="org-form-group">
<label class="org-form-label">법인명</label>
<div class="org-form-input-wrapper">
<input type="text" name="companyName" class="org-form-input" th:value="${orgName}" readonly>
</div>
</div>
</div>
</div>
<!-- Basic Information Section -->
<div class="org-section-header org-section-header--agreement" th:style="${isInvited ? 'margin-top: 48px;' : ''}">
<h3>기본 정보</h3>
<span class="required-badge">필수 입력</span>
</div>
<!-- Basic Information Section -->
<div class="org-section-header org-section-header--agreement" th:style="${isInvited ? 'margin-top: 48px;' : ''}">
<h3>기본 정보</h3>
<span class="required-badge">필수 입력</span>
</div>
<div th:if="${registrationType == (isInvited ? 'corporate' : 'personal')}">
<th:block th:replace="~{apps/register/components/commonUserInfoForm :: commonUserInfo}"></th:block>
<th:block th:replace="~{apps/register/components/newUserInfoForm :: newUserForm}"></th:block>
</div>
<div th:if="${registrationType == (isInvited ? 'corporate' : 'personal')}">
<th:block th:replace="~{apps/register/components/commonUserInfoForm :: commonUserInfo}"></th:block>
<th:block th:replace="~{apps/register/components/newUserInfoForm :: newUserForm}"></th:block>
</div>
<!-- Action Buttons -->
<div class="org-action-buttons">
<button type="button" class="btn btn-secondary" id="cancelButton">취소</button>
<button type="button" class="btn btn-primary btn_register">가입신청</button>
</div>
</form>
<!-- Action Buttons -->
<div class="org-action-buttons">
<button type="button" class="btn btn-secondary" id="cancelButton">취소</button>
<button type="button" class="btn btn-primary btn_register">가입신청</button>
</div>
</form>
</div>
<!-- Loading Overlay -->
<div class="org-loading-overlay" id="loadingOverlay">