회원가입 디자인 적용

This commit is contained in:
현성필
2025-11-15 18:38:03 +09:00
parent cadac012e6
commit 42100e8a1a
18 changed files with 3088 additions and 1398 deletions
+748
View File
@@ -10558,6 +10558,754 @@ select.form-control {
} }
} }
.org-page-title-banner {
position: relative;
width: 100%;
height: 220px;
background: linear-gradient(to right, #E8F4FD 0%, #F0F8FF 100%);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 64px;
overflow: hidden;
}
.org-page-title-banner::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0h100v100H0z' fill='%23ffffff' opacity='0.03'/%3E%3C/svg%3E");
background-size: 50px 50px;
opacity: 0.5;
}
.org-page-title-banner .title-image {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
height: 220px;
width: auto;
z-index: 1;
}
.org-page-title-banner h1 {
position: relative;
font-size: 40px;
font-weight: 700;
color: #1A1A2E;
margin: 0;
z-index: 2;
letter-spacing: -0.5px;
}
@media (max-width: 768px) {
.org-page-title-banner {
height: 120px;
margin-bottom: 32px;
}
.org-page-title-banner .title-image {
height: 120px;
}
.org-page-title-banner h1 {
font-size: 24px;
}
}
.org-register-page {
min-height: 100vh;
padding: 0;
}
@media (max-width: 768px) {
.org-register-page {
padding: 0;
}
}
.org-register-container {
max-width: 1066px;
margin: 0 auto;
padding: 0 24px 64px;
}
@media (max-width: 768px) {
.org-register-container {
padding: 0 24px 32px;
}
}
.org-register-title {
font-size: 40px;
font-weight: 700;
color: #1A1A2E;
text-align: center;
margin-bottom: 48px;
}
@media (max-width: 768px) {
.org-register-title {
font-size: 32px;
margin-bottom: 32px;
}
}
.org-section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px 32px;
background: linear-gradient(135deg, #4B9BFF 0%, #2E7FF7 100%);
border-radius: 8px;
margin-bottom: 32px;
}
.org-section-header h3 {
font-size: 20px;
font-weight: 600;
color: #FFFFFF;
margin: 0;
}
.org-section-header .required-badge {
background: rgba(255, 255, 255, 0.2);
color: #FFFFFF;
padding: 4px 16px;
border-radius: 50px;
font-size: 14px;
font-weight: 500;
display: inline-flex;
align-items: center;
gap: 4px;
}
.org-section-header .required-badge::before {
content: "*";
color: #FFD93D;
font-weight: 700;
}
.org-section-header--agreement {
background: none;
padding: 0 0 16px 0;
border-radius: 0;
border-bottom: 2px solid #212529;
margin-bottom: 32px;
}
.org-section-header--agreement h3 {
font-size: 22px;
font-weight: 700;
color: #212529;
margin: 0;
}
.org-section-header--agreement .required-badge {
display: none;
}
@media (max-width: 768px) {
.org-section-header {
padding: 16px 24px;
}
.org-section-header h3 {
font-size: 18px;
}
.org-section-header--agreement {
padding: 0 0 8px 0;
}
.org-section-header--agreement h3 {
font-size: 18px;
}
}
.org-info-notice {
background: linear-gradient(135deg, #EFF6FF 0%, #F0F9FF 100%);
padding: 24px;
border-radius: 8px;
margin-bottom: 32px;
}
.org-info-notice ul {
list-style: none;
padding: 0;
margin: 0;
}
.org-info-notice ul li {
position: relative;
padding-left: 24px;
color: #64748B;
font-size: 14px;
line-height: 1.6;
}
.org-info-notice ul li::before {
content: "•";
position: absolute;
left: 0;
color: #4B9BFF;
font-weight: 700;
}
.org-info-notice ul li + li {
margin-top: 8px;
}
.org-form-group {
margin-bottom: 40px;
display: flex;
align-items: flex-start;
gap: 24px;
}
.org-form-group:last-child {
margin-bottom: 0;
}
@media (max-width: 768px) {
.org-form-group {
flex-direction: column;
gap: 16px;
}
}
.org-form-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
color: #1A1A2E;
min-width: 180px;
padding-top: 16px;
}
.org-form-label .required {
color: #FF6B6B;
font-weight: 700;
}
@media (max-width: 768px) {
.org-form-label {
min-width: auto;
padding-top: 0;
}
}
.org-form-input-wrapper {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.org-form-input {
width: 100%;
padding: 16px 24px;
border: 2px solid #E2E8F0;
border-radius: 8px;
font-size: 16px;
color: #1A1A2E;
transition: all 0.3s ease;
background: #FFFFFF;
}
.org-form-input:focus {
outline: none;
border-color: #4B9BFF;
box-shadow: 0 0 0 3px rgba(75, 155, 255, 0.1);
}
.org-form-input:disabled {
background: #F8FAFC;
color: #94A3B8;
cursor: not-allowed;
}
.org-form-input.is-invalid {
border-color: #FF6B6B;
}
.org-form-input.is-valid {
border-color: #6BCF7F;
}
.org-form-input::placeholder {
color: #94A3B8;
}
.org-compound-input {
display: flex;
align-items: center;
gap: 8px;
}
.org-compound-input .org-form-input,
.org-compound-input .org-form-select {
flex: 1;
min-width: 0;
}
.org-compound-input .phone-number {
flex: 1;
width: 100%;
}
.org-compound-input .separator {
color: #64748B;
font-weight: 600;
user-select: none;
}
@media (max-width: 768px) {
.org-compound-input {
flex-wrap: wrap;
}
.org-compound-input .org-form-input,
.org-compound-input .org-form-select {
min-width: 80px;
}
}
.org-form-select {
width: 100%;
padding: 16px 24px;
border: 2px solid #E2E8F0;
border-radius: 8px;
font-size: 16px;
color: #1A1A2E;
background: #FFFFFF;
cursor: pointer;
transition: all 0.3s ease;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2364748B' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 24px center;
padding-right: 48px;
}
.org-form-select:focus {
outline: none;
border-color: #4B9BFF;
box-shadow: 0 0 0 3px rgba(75, 155, 255, 0.1);
}
.org-form-select:disabled {
background-color: #F8FAFC;
color: #94A3B8;
cursor: not-allowed;
}
.org-input-button-group {
display: flex;
gap: 16px;
}
.org-input-button-group .org-form-input {
flex: 1;
}
@media (max-width: 768px) {
.org-input-button-group {
flex-direction: column;
}
.org-input-button-group .org-form-input {
flex: none;
}
}
.org-auth-input-group {
position: relative;
display: flex;
align-items: center;
}
.org-auth-input-group .org-form-input {
padding-right: 100px;
}
.org-timer {
position: absolute;
right: 24px;
top: 50%;
transform: translateY(-50%);
font-size: 14px;
font-weight: 600;
color: #FF6B6B;
pointer-events: none;
user-select: none;
}
.org-btn {
padding: 16px 40px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
border: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
white-space: nowrap;
}
.org-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.org-btn:not(:disabled):hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(75, 155, 255, 0.1);
}
.org-btn:not(:disabled):active {
transform: translateY(0);
}
.org-btn-primary {
background: linear-gradient(135deg, #4B9BFF 0%, #2E7FF7 100%);
color: #FFFFFF;
}
.org-btn-primary:not(:disabled):hover {
background: linear-gradient(135deg, #2E7FF7 0%, #4B9BFF 100%);
}
.org-btn-secondary {
background: #FFFFFF;
color: #64748B;
border: 2px solid #E2E8F0;
}
.org-btn-secondary:not(:disabled):hover {
border-color: #4B9BFF;
color: #4B9BFF;
}
.org-btn-check, .org-file-label {
padding: 16px 24px;
background: #FFFFFF;
color: #4B9BFF;
border: 2px solid #4B9BFF;
}
.org-btn-check:not(:disabled):hover, .org-file-label:not(:disabled):hover {
background: #4B9BFF;
color: #FFFFFF;
}
.org-file-upload {
display: flex;
align-items: center;
gap: 16px;
}
@media (max-width: 768px) {
.org-file-upload {
flex-direction: column;
align-items: stretch;
}
}
.org-file-display {
flex: 1;
padding: 16px 24px;
border: 2px dashed #E2E8F0;
border-radius: 8px;
background: #F8FAFC;
color: #94A3B8;
cursor: default;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.org-file-display.has-file {
border-style: solid;
border-color: #6BCF7F;
background: rgba(107, 207, 127, 0.05);
color: #1A1A2E;
}
.org-file-label {
cursor: pointer;
}
.org-file-label input[type=file] {
display: none;
}
.org-file-remove {
padding: 16px;
background: #FF6B6B;
color: #FFFFFF;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
}
.org-file-remove:hover {
background: #ff3838;
}
.org-file-notice {
margin-top: 16px;
padding: 16px;
background: linear-gradient(135deg, #FFF7ED 0%, #FFFBEB 100%);
border-radius: 6px;
}
.org-file-notice p {
margin: 0;
font-size: 14px;
color: #64748B;
line-height: 1.5;
}
.org-file-notice p + p {
margin-top: 4px;
}
.org-file-notice p::before {
content: "• ";
color: #FFD93D;
font-weight: 700;
}
.org-action-buttons {
display: flex;
gap: 24px;
justify-content: center;
margin-top: 64px;
padding-top: 48px;
border-top: 1px solid #E2E8F0;
}
@media (max-width: 768px) {
.org-action-buttons {
flex-direction: column;
margin-top: 40px;
padding-top: 32px;
}
}
.org-validation-message {
margin-top: 8px;
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
}
.org-validation-message.success {
background: rgba(107, 207, 127, 0.1);
color: #6BCF7F;
}
.org-validation-message.error {
background: rgba(255, 107, 107, 0.1);
color: #FF6B6B;
}
.org-loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(26, 26, 46, 0.7);
display: none;
align-items: center;
justify-content: center;
z-index: 9999;
}
.org-loading-overlay.active {
display: flex;
}
.org-loading-overlay .spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(255, 255, 255, 0.2);
border-top-color: #FFFFFF;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@media (max-width: 768px) {
.org-compound-input .org-form-select,
.org-compound-input .org-form-input {
font-size: 14px;
padding: 8px 16px;
}
}
.agreement-form {
background: #FFFFFF;
border: 1px solid #DDDDDD;
border-radius: 12px;
padding: 30px 20px;
display: flex;
flex-direction: column;
gap: 20px;
margin-bottom: 90px;
}
.agreement-all-section {
display: flex;
align-items: center;
padding: 10px 0;
}
.agreement-divider {
height: 1px;
background: #DDDDDD;
width: 100%;
}
.agreement-items-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.agreement-item-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
height: 28px;
}
.agreement-checkbox-label {
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
flex: 1;
user-select: none;
}
.agreement-checkbox-input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.agreement-checkbox-input:checked + .agreement-checkbox-custom {
background-image: url("/img/checkbox-checked.png");
}
.agreement-checkbox-input:focus + .agreement-checkbox-custom {
box-shadow: 0 0 0 3px rgba(140, 149, 159, 0.1);
}
.agreement-checkbox-custom {
position: relative;
width: 26px;
height: 26px;
min-width: 26px;
border: none;
border-radius: 50%;
background-image: url("/img/checkbox-unchecked.png");
background-size: contain;
background-repeat: no-repeat;
background-position: center;
transition: all 0.3s ease;
padding: 0;
}
.agreement-checkbox-text {
font-size: 16px;
font-weight: 400;
color: #515961;
line-height: 100.02%;
display: flex;
align-items: center;
gap: 4px;
}
.agreement-checkbox-text.agreement-all-text {
font-size: 18px;
font-weight: 700;
color: #212529;
}
.agreement-required {
color: #0049B4;
font-weight: 700;
}
.agreement-title {
color: #515961;
}
.agreement-toggle-icon {
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
min-width: 22px;
border: none;
background: transparent;
color: #64748B;
cursor: pointer;
transition: all 0.3s ease;
padding: 8px 4px 10px 7px;
}
.agreement-toggle-icon i {
font-size: 12px;
transition: transform 0.3s ease;
}
.agreement-toggle-icon:hover {
color: #4B9BFF;
}
.agreement-toggle-icon.active {
color: #4B9BFF;
}
.agreement-toggle-icon.active i {
transform: rotate(180deg);
}
.agreement-content {
margin-top: 16px;
background: #EDF9FE;
}
.agreement-scroll {
max-height: 300px;
overflow-y: auto;
padding: 24px;
}
.agreement-scroll::-webkit-scrollbar {
width: 8px;
}
.agreement-scroll::-webkit-scrollbar-track {
background: #E2E8F0;
border-radius: 6px;
}
.agreement-scroll::-webkit-scrollbar-thumb {
background: #94A3B8;
border-radius: 6px;
}
.agreement-scroll::-webkit-scrollbar-thumb:hover {
background: #64748B;
}
.agreement-scroll div {
font-size: 14px;
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 {
color: #1A1A2E;
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
}
.agreement-scroll div p {
margin-bottom: 16px;
}
.agreement-scroll div ul, .agreement-scroll div ol {
margin-left: 24px;
margin-bottom: 16px;
}
.agreement-scroll div table {
width: 100%;
border-collapse: collapse;
margin-bottom: 16px;
}
.agreement-scroll div table th, .agreement-scroll div table td {
border: 1px solid #E2E8F0;
padding: 8px;
text-align: left;
}
.agreement-scroll div table th {
background: #EFF6FF;
font-weight: 600;
}
@media (max-width: 768px) {
.agreement-form {
padding: 20px 16px;
gap: 16px;
}
.agreement-all-section {
padding: 8px 0;
}
.agreement-items-list {
gap: 16px;
}
.agreement-item-row {
height: auto;
min-height: 28px;
}
.agreement-checkbox-text {
font-size: 14px;
}
.agreement-checkbox-text.agreement-all-text {
font-size: 16px;
}
.agreement-scroll {
max-height: 250px;
padding: 16px;
}
}
.user-management-container { .user-management-container {
max-width: 1400px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 808 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 990 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

@@ -13,6 +13,8 @@ $accent-orange: #FF6B6B; // Orange accent
$accent-green: #6BCF7F; // Green accent $accent-green: #6BCF7F; // Green accent
$accent-purple: #A78BFA; // Purple accent $accent-purple: #A78BFA; // Purple accent
$main-bg: #EDF9FE;
// Neutral colors // Neutral colors
$text-dark: #1A1A2E; // Main text color $text-dark: #1A1A2E; // Main text color
$text-gray: #64748B; // Secondary text $text-gray: #64748B; // Secondary text
@@ -25,6 +27,7 @@ $black: #000000; // Pure black
$gray-800: #1F2937; // Dark gray $gray-800: #1F2937; // Dark gray
$gray-900: #111827; // Darker gray $gray-900: #111827; // Darker gray
// Gradients // Gradients
$gradient-primary: linear-gradient(135deg, $primary-blue 0%, $secondary-blue 100%); $gradient-primary: linear-gradient(135deg, $primary-blue 0%, $secondary-blue 100%);
$gradient-accent: linear-gradient(135deg, $accent-cyan 0%, $primary-blue 100%); $gradient-accent: linear-gradient(135deg, $accent-cyan 0%, $primary-blue 100%);
@@ -118,4 +121,4 @@ $button-padding-y: 16px;
$card-padding: 32px; $card-padding: 32px;
$input-padding-x: 16px; $input-padding-x: 16px;
$input-padding-y: 12px; $input-padding-y: 12px;
$input-height: 48px; $input-height: 48px;
+1
View File
@@ -48,6 +48,7 @@
@import 'pages/apikey-detail'; @import 'pages/apikey-detail';
@import 'pages/notice'; @import 'pages/notice';
@import 'pages/inquiry'; @import 'pages/inquiry';
@import 'pages/org-register';
@import 'pages/user-management'; @import 'pages/user-management';
// 6. Themes // 6. Themes
@@ -0,0 +1,857 @@
// Organization Registration Page Styles
// Modern design for corporate registration flow
@import '../abstracts/variables';
@import '../abstracts/mixins';
// Page Title Banner
.org-page-title-banner {
position: relative;
width: 100%;
height: 220px;
background: linear-gradient(to right, #E8F4FD 0%, #F0F8FF 100%);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: $spacing-4xl;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0h100v100H0z' fill='%23ffffff' opacity='0.03'/%3E%3C/svg%3E");
background-size: 50px 50px;
opacity: 0.5;
}
.title-image {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
height: 220px;
width: auto;
z-index: 1;
}
h1 {
position: relative;
font-size: $font-size-3xl;
font-weight: $font-weight-bold;
color: $text-dark;
margin: 0;
z-index: 2;
letter-spacing: -0.5px;
}
@include respond-to('sm') {
height: 120px;
margin-bottom: $spacing-xl;
.title-image {
height: 120px;
}
h1 {
font-size: $font-size-xl;
}
}
}
// Main Registration Container
.org-register-page {
min-height: 100vh;
padding: 0;
@include respond-to('sm') {
padding: 0;
}
}
.org-register-container {
max-width: 1066px;
margin: 0 auto;
padding: 0 $spacing-lg $spacing-4xl;
@include respond-to('sm') {
padding: 0 $spacing-lg $spacing-xl;
}
}
// Header
.org-register-title {
font-size: $font-size-3xl;
font-weight: $font-weight-bold;
color: $text-dark;
text-align: center;
margin-bottom: $spacing-3xl;
@include respond-to('sm') {
font-size: $font-size-2xl;
margin-bottom: $spacing-xl;
}
}
// Section Headers
.org-section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: $spacing-lg $spacing-xl;
background: linear-gradient(135deg, $primary-blue 0%, $secondary-blue 100%);
border-radius: $border-radius-md;
margin-bottom: $spacing-xl;
h3 {
font-size: $font-size-lg;
font-weight: $font-weight-semibold;
color: $white;
margin: 0;
}
.required-badge {
background: rgba(255, 255, 255, 0.2);
color: $white;
padding: $spacing-xs $spacing-md;
border-radius: $border-radius-full;
font-size: $font-size-sm;
font-weight: $font-weight-medium;
display: inline-flex;
align-items: center;
gap: $spacing-xs;
&::before {
content: '*';
color: $accent-yellow;
font-weight: $font-weight-bold;
}
}
// Agreement variant - Simple text with underline (Figma design)
&--agreement {
background: none;
padding: 0 0 $spacing-md 0;
border-radius: 0;
border-bottom: 2px solid #212529;
margin-bottom: $spacing-xl;
h3 {
font-size: 22px;
font-weight: $font-weight-bold;
color: #212529;
margin: 0;
}
.required-badge {
display: none;
}
}
@include respond-to('sm') {
padding: $spacing-md $spacing-lg;
h3 {
font-size: $font-size-md;
}
&--agreement {
padding: 0 0 $spacing-sm 0;
h3 {
font-size: $font-size-md;
}
}
}
}
// Info Notice
.org-info-notice {
background: linear-gradient(135deg, #EFF6FF 0%, #F0F9FF 100%);
padding: $spacing-lg;
border-radius: $border-radius-md;
margin-bottom: $spacing-xl;
ul {
list-style: none;
padding: 0;
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: $spacing-2xl;
display: flex;
align-items: flex-start;
gap: $spacing-lg;
&:last-child {
margin-bottom: 0;
}
@include respond-to('sm') {
flex-direction: column;
gap: $spacing-md;
}
}
.org-form-label {
display: flex;
align-items: center;
gap: $spacing-sm;
font-size: $font-size-base;
font-weight: $font-weight-semibold;
color: $text-dark;
min-width: 180px;
padding-top: $spacing-md;
.required {
color: $accent-orange;
font-weight: $font-weight-bold;
}
@include respond-to('sm') {
min-width: auto;
padding-top: 0;
}
}
.org-form-input-wrapper {
flex: 1;
display: flex;
flex-direction: column;
gap: $spacing-sm;
}
// Input Fields
.org-form-input {
width: 100%;
padding: $spacing-md $spacing-lg;
border: 2px solid $border-gray;
border-radius: $border-radius-md;
font-size: $font-size-base;
color: $text-dark;
transition: $transition-base;
background: $white;
&:focus {
outline: none;
border-color: $primary-blue;
box-shadow: 0 0 0 3px rgba(75, 155, 255, 0.1);
}
&:disabled {
background: $gray-bg;
color: $text-light;
cursor: not-allowed;
}
&.is-invalid {
border-color: $accent-orange;
}
&.is-valid {
border-color: $accent-green;
}
&::placeholder {
color: $text-light;
}
}
// Compound Inputs (e.g., phone number, email)
.org-compound-input {
display: flex;
align-items: center;
gap: $spacing-sm;
.org-form-input,
.org-form-select {
flex: 1;
min-width: 0;
}
// Equal width for phone number inputs
.phone-number {
flex: 1;
width: 100%;
}
.separator {
color: $text-gray;
font-weight: $font-weight-semibold;
user-select: none;
}
@include respond-to('sm') {
flex-wrap: wrap;
.org-form-input,
.org-form-select {
min-width: 80px;
}
}
}
// Select Dropdown
.org-form-select {
width: 100%;
padding: $spacing-md $spacing-lg;
border: 2px solid $border-gray;
border-radius: $border-radius-md;
font-size: $font-size-base;
color: $text-dark;
background: $white;
cursor: pointer;
transition: $transition-base;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2364748B' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right $spacing-lg center;
padding-right: $spacing-3xl;
&:focus {
outline: none;
border-color: $primary-blue;
box-shadow: 0 0 0 3px rgba(75, 155, 255, 0.1);
}
&:disabled {
background-color: $gray-bg;
color: $text-light;
cursor: not-allowed;
}
}
// Button with Input
.org-input-button-group {
display: flex;
gap: $spacing-md;
.org-form-input {
flex: 1;
}
@include respond-to('sm') {
flex-direction: column;
.org-form-input {
flex: none;
}
}
}
// Auth Input Group (for authentication number with timer)
.org-auth-input-group {
position: relative;
display: flex;
align-items: center;
.org-form-input {
padding-right: 100px; // Make space for timer
}
}
.org-timer {
position: absolute;
right: $spacing-lg;
top: 50%;
transform: translateY(-50%);
font-size: $font-size-sm;
font-weight: $font-weight-semibold;
color: $accent-orange;
pointer-events: none;
user-select: none;
}
// Buttons
.org-btn {
padding: $spacing-md $spacing-2xl;
border-radius: $border-radius-md;
font-size: $font-size-base;
font-weight: $font-weight-semibold;
cursor: pointer;
transition: $transition-base;
border: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: $spacing-sm;
white-space: nowrap;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
&:not(:disabled):hover {
transform: translateY(-1px);
box-shadow: $shadow-md;
}
&:not(:disabled):active {
transform: translateY(0);
}
}
.org-btn-primary {
background: linear-gradient(135deg, $primary-blue 0%, $secondary-blue 100%);
color: $white;
&:not(:disabled):hover {
background: linear-gradient(135deg, $secondary-blue 0%, $primary-blue 100%);
}
}
.org-btn-secondary {
background: $white;
color: $text-gray;
border: 2px solid $border-gray;
&:not(:disabled):hover {
border-color: $primary-blue;
color: $primary-blue;
}
}
.org-btn-check {
padding: $spacing-md $spacing-lg;
background: $white;
color: $primary-blue;
border: 2px solid $primary-blue;
&:not(:disabled):hover {
background: $primary-blue;
color: $white;
}
}
// File Upload
.org-file-upload {
display: flex;
align-items: center;
gap: $spacing-md;
@include respond-to('sm') {
flex-direction: column;
align-items: stretch;
}
}
.org-file-display {
flex: 1;
padding: $spacing-md $spacing-lg;
border: 2px dashed $border-gray;
border-radius: $border-radius-md;
background: $gray-bg;
color: $text-light;
cursor: default;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&.has-file {
border-style: solid;
border-color: $accent-green;
background: rgba(107, 207, 127, 0.05);
color: $text-dark;
}
}
.org-file-label {
@extend .org-btn-check;
cursor: pointer;
input[type="file"] {
display: none;
}
}
.org-file-remove {
padding: $spacing-md;
background: $accent-orange;
color: $white;
border: none;
border-radius: $border-radius-md;
cursor: pointer;
transition: $transition-base;
&:hover {
background: darken($accent-orange, 10%);
}
}
// File Upload Notice
.org-file-notice {
margin-top: $spacing-md;
padding: $spacing-md;
background: linear-gradient(135deg, #FFF7ED 0%, #FFFBEB 100%);
border-radius: $border-radius-sm;
p {
margin: 0;
font-size: $font-size-sm;
color: $text-gray;
line-height: 1.5;
& + p {
margin-top: $spacing-xs;
}
&::before {
content: '';
color: $accent-yellow;
font-weight: $font-weight-bold;
}
}
}
// Action Buttons
.org-action-buttons {
display: flex;
gap: $spacing-lg;
justify-content: center;
margin-top: $spacing-4xl;
padding-top: $spacing-3xl;
border-top: 1px solid $border-gray;
@include respond-to('sm') {
flex-direction: column;
margin-top: $spacing-2xl;
padding-top: $spacing-xl;
}
}
// Validation Messages
.org-validation-message {
margin-top: $spacing-sm;
padding: $spacing-sm $spacing-md;
border-radius: $border-radius-sm;
font-size: $font-size-sm;
&.success {
background: rgba(107, 207, 127, 0.1);
color: $accent-green;
}
&.error {
background: rgba(255, 107, 107, 0.1);
color: $accent-orange;
}
}
// Loading Overlay
.org-loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(26, 26, 46, 0.7);
display: none;
align-items: center;
justify-content: center;
z-index: 9999;
&.active {
display: flex;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(255, 255, 255, 0.2);
border-top-color: $white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
// Responsive Adjustments
@include respond-to('sm') {
.org-compound-input {
.org-form-select,
.org-form-input {
font-size: $font-size-sm;
padding: $spacing-sm $spacing-md;
}
}
}
// Agreement Form - Figma Design
.agreement-form {
background: $white;
border: 1px solid #DDDDDD;
border-radius: 12px;
padding: 30px 20px;
display: flex;
flex-direction: column;
gap: 20px;
margin-bottom: 90px;
}
// Agree All Section
.agreement-all-section {
display: flex;
align-items: center;
padding: 10px 0;
}
// Divider
.agreement-divider {
height: 1px;
background: #DDDDDD;
width: 100%;
}
// Agreement Items List
.agreement-items-list {
display: flex;
flex-direction: column;
gap: 20px;
}
// Agreement Item Row
.agreement-item-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
height: 28px;
}
// Checkbox Label
.agreement-checkbox-label {
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
flex: 1;
user-select: none;
}
// Checkbox Input (hidden)
.agreement-checkbox-input {
position: absolute;
opacity: 0;
pointer-events: none;
&:checked + .agreement-checkbox-custom {
background-image: url('/img/checkbox-checked.png');
}
&:focus + .agreement-checkbox-custom {
box-shadow: 0 0 0 3px rgba(140, 149, 159, 0.1);
}
}
// Checkbox Custom Style (Circle Design)
.agreement-checkbox-custom {
position: relative;
width: 26px;
height: 26px;
min-width: 26px;
border: none;
border-radius: 50%;
background-image: url('/img/checkbox-unchecked.png');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
transition: $transition-base;
padding: 0;
}
// Checkbox Text
.agreement-checkbox-text {
font-size: 16px;
font-weight: 400;
color: #515961;
line-height: 100.02%;
display: flex;
align-items: center;
gap: 4px;
&.agreement-all-text {
font-size: 18px;
font-weight: 700;
color: #212529;
}
}
// Agreement Required Badge
.agreement-required {
color: #0049B4;
font-weight: 700;
}
// Agreement Title
.agreement-title {
color: #515961;
}
// Toggle Icon
.agreement-toggle-icon {
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
min-width: 22px;
border: none;
background: transparent;
color: $text-gray;
cursor: pointer;
transition: $transition-base;
padding: 8px 4px 10px 7px;
i {
font-size: 12px;
transition: transform 0.3s ease;
}
&:hover {
color: $primary-blue;
}
&.active {
color: $primary-blue;
i {
transform: rotate(180deg);
}
}
}
// Agreement Content
.agreement-content {
margin-top: $spacing-md;
background: $main-bg;
}
.agreement-scroll {
max-height: 300px;
overflow-y: auto;
padding: $spacing-lg;
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: $border-gray;
border-radius: $border-radius-sm;
}
&::-webkit-scrollbar-thumb {
background: $text-light;
border-radius: $border-radius-sm;
&:hover {
background: $text-gray;
}
}
// Style agreement content
div {
font-size: $font-size-sm;
line-height: 1.8;
color: $text-gray;
h1, h2, h3, h4, h5, h6 {
color: $text-dark;
margin-top: $spacing-lg;
margin-bottom: $spacing-md;
font-weight: $font-weight-semibold;
}
p {
margin-bottom: $spacing-md;
}
ul, ol {
margin-left: $spacing-lg;
margin-bottom: $spacing-md;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: $spacing-md;
th, td {
border: 1px solid $border-gray;
padding: $spacing-sm;
text-align: left;
}
th {
background: $light-bg;
font-weight: $font-weight-semibold;
}
}
}
}
// Responsive
@include respond-to('sm') {
.agreement-form {
padding: 20px 16px;
gap: 16px;
}
.agreement-all-section {
padding: 8px 0;
}
.agreement-items-list {
gap: 16px;
}
.agreement-item-row {
height: auto;
min-height: 28px;
}
.agreement-checkbox-text {
font-size: $font-size-sm;
&.agreement-all-text {
font-size: $font-size-base;
}
}
.agreement-scroll {
max-height: 250px;
padding: $spacing-md;
}
}
@@ -1,102 +1,83 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" <html lang="ko" 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}">
xmlns:hx-on="http://www.w3.org/1999/xhtml">
<body> <body>
<th:block th:fragment="commonUserInfo"> <th:block th:fragment="commonUserInfo">
<div class="info1 pt28"> <div class="org-form-group">
<p class="title"> <label class="org-form-label">
<span class="dot">이메일 아이디</span> <span class="required">*</span>
</p> 이메일 아이디
<div class="info_line"> </label>
<div class="info_box1"> <div class="org-form-input-wrapper">
<input type="text" id="userId" class="common_input_type_1 w_inp" <div class="org-compound-input">
<input type="text" id="userId" class="org-form-input"
th:classappend="${isInvited ? 'input-readonly' : ''}" th:classappend="${isInvited ? 'input-readonly' : ''}"
maxlength="30" maxlength="30"
th:value="${emailId}" th:value="${emailId}"
th:readonly="${isInvited}" th:readonly="${isInvited}"
th:placeholder="#{portalUser.Register.email}"> th:placeholder="#{portalUser.Register.email}">
<span class="special_t">@</span> <span class="separator">@</span>
<input type="text" id="domain" class="common_input_type_1 w_inp2" <input type="text" id="domain" class="org-form-input"
th:classappend="${isInvited ? 'input-readonly' : ''}" th:classappend="${isInvited ? 'input-readonly' : ''}"
maxlength="50" maxlength="50"
th:value="${domain}" th:value="${domain}"
th:readonly="${isInvited}" th:readonly="${isInvited}"
th:placeholder="#{portalUser.Register.domain}"> th:placeholder="#{portalUser.Register.domain}">
<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>
<div class="btn_check">
<a class="common_btn_type_2 cbt btn_check_email"
th:classappend="${isInvited ? 'hidden' : ''}"
th:hx-post="@{/check_id}"
hx-include="#csrf, #loginId, [name='registrationType']"
hx-target="#email-validation"
hx-swap="none">
<span>중복체크</span>
</a>
</div> </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"/>
<button type="button" class="org-btn org-btn-check btn_check_email"
th:classappend="${isInvited ? 'hidden' : ''}">
중복체크
</button>
<div id="email-validation" class="org-validation-message"></div>
</div> </div>
<div id="email-validation"></div>
</div> </div>
</th:block> </th:block>
</body> </body>
<th:block> <th:block>
<script th:fragment="commonUserInfoScript"> <script th:fragment="commonUserInfoScript" th:inline="javascript">
// 이메일 조합 함수 // 이메일 조합 함수
function combineEmail() { function combineEmail() {
let emailId = document.getElementById('userId').value; let emailId = $('#userId').val();
let domain = document.getElementById('domain').value; let domain = $('#domain').val();
let combined = emailId + '@' + domain; let combined = emailId + '@' + domain;
let loginIdInput = document.getElementById('loginId'); $('#loginId').val(combined);
if (loginIdInput) {
loginIdInput.value = combined;
}
} }
function handleEmailValidationResponse(response) { function handleEmailValidationResponse(response) {
const individualConversionForm = document.getElementById('individualConversionForm'); const individualConversionForm = $('#individualConversionForm');
const emailChangeForm = document.getElementById('emailChangeForm'); const emailChangeForm = $('#emailChangeForm');
const newUserForm = document.getElementById('newUserForm'); const newUserForm = $('#newUserForm, #newUserFields');
const userIdInput = document.getElementById('userId'); const userIdInput = $('#userId');
const domainInput = document.getElementById('domain'); const domainInput = $('#domain');
const checkButton = document.querySelector('.btn_check_email'); const checkButton = $('.btn_check_email');
const loginIdInput = document.getElementById('loginId'); const loginIdInput = $('#loginId');
const scenarioInput = document.getElementById('registrationScenario'); const scenarioInput = $('#registrationScenario');
if (response && response.showPopup) { if (response && response.showPopup) {
switch (response.popupType) { switch (response.popupType) {
case "validEmail": case "validEmail":
case "newUser": case "newUser":
// 시나리오 값 설정 // 시나리오 값 설정
if (scenarioInput) { scenarioInput.val('new');
scenarioInput.value = 'new';
// hidden input이 form의 일부인지 확인
if (!scenarioInput.form) {
document.getElementById('registerForm').appendChild(scenarioInput);
}
}
customPopups.showAlert(response.message); customPopups.showAlert(response.message);
if (checkButton) { checkButton.hide();
checkButton.style.display = 'none';
}
if (userIdInput && domainInput && loginIdInput) { userIdInput.prop('disabled', true);
userIdInput.disabled = true; domainInput.prop('disabled', true);
domainInput.disabled = true; userIdInput.css('backgroundColor', '#e0e0e0');
userIdInput.style.backgroundColor = '#e0e0e0'; domainInput.css('backgroundColor', '#e0e0e0');
domainInput.style.backgroundColor = '#e0e0e0';
const combinedEmail = `${userIdInput.value.trim()}@${domainInput.value.trim()}`; const combinedEmail = `${userIdInput.val().trim()}@${domainInput.val().trim()}`;
loginIdInput.value = combinedEmail; loginIdInput.val(combinedEmail);
}
if (newUserForm) newUserForm.style.display = 'block'; newUserForm.show();
if (individualConversionForm) individualConversionForm.style.display = 'none'; individualConversionForm.hide();
if (emailChangeForm) emailChangeForm.style.display = 'none'; emailChangeForm.hide();
break; break;
case "conversionOrChange": case "conversionOrChange":
@@ -104,51 +85,33 @@
response.message, response.message,
function () { function () {
// "이메일 유지" 선택 시 동작 // "이메일 유지" 선택 시 동작
if (scenarioInput) { scenarioInput.val('retain');
scenarioInput.value = 'retain';
if (!scenarioInput.form) {
document.getElementById('registerForm').appendChild(scenarioInput);
}
}
if (individualConversionForm) individualConversionForm.style.display = 'block'; individualConversionForm.show();
if (emailChangeForm) emailChangeForm.style.display = 'none'; emailChangeForm.hide();
if (newUserForm) newUserForm.style.display = 'none'; newUserForm.hide();
if (userIdInput && domainInput) { userIdInput.prop('disabled', true);
userIdInput.disabled = true; domainInput.prop('disabled', true);
domainInput.disabled = true; userIdInput.css('backgroundColor', '#e0e0e0');
userIdInput.style.backgroundColor = '#e0e0e0'; domainInput.css('backgroundColor', '#e0e0e0');
domainInput.style.backgroundColor = '#e0e0e0';
}
if (checkButton) { checkButton.hide();
checkButton.style.display = 'none';
}
}, },
function () { function () {
// "이메일 전환" 선택 시 동작 // "이메일 전환" 선택 시 동작
if (scenarioInput) { scenarioInput.val('change');
scenarioInput.value = 'change';
if (!scenarioInput.form) {
document.getElementById('registerForm').appendChild(scenarioInput);
}
}
if (emailChangeForm) emailChangeForm.style.display = 'block'; emailChangeForm.show();
if (individualConversionForm) individualConversionForm.style.display = 'none'; individualConversionForm.hide();
if (newUserForm) newUserForm.style.display = 'none'; newUserForm.hide();
if (userIdInput && domainInput) { userIdInput.prop('disabled', true);
userIdInput.disabled = true; domainInput.prop('disabled', true);
domainInput.disabled = true; userIdInput.css('backgroundColor', '#e0e0e0');
userIdInput.style.backgroundColor = '#e0e0e0'; domainInput.css('backgroundColor', '#e0e0e0');
domainInput.style.backgroundColor = '#e0e0e0';
}
if (checkButton) { checkButton.hide();
checkButton.style.display = 'none';
}
} }
); );
break; break;
@@ -169,11 +132,35 @@
} }
// 이메일 입력 필드에 이벤트 리스너 추가 // 이메일 입력 필드에 이벤트 리스너 추가
document.addEventListener('DOMContentLoaded', function () { $(document).ready(function () {
let userIdInput = document.getElementById("userId"); $('#userId, #domain').on('input', combineEmail);
let domainInput = document.getElementById("domain");
if (userIdInput) userIdInput.addEventListener("input", combineEmail); // 이메일 중복 체크 버튼 클릭
if (domainInput) domainInput.addEventListener("input", combineEmail); $('.btn_check_email').on('click', function () {
const loginId = $('#loginId').val();
const registrationType = $('input[name="registrationType"]').val();
if (!loginId || loginId.indexOf('@') === -1) {
customPopups.showAlert('이메일 주소를 올바르게 입력해주세요.');
return;
}
$.ajax({
url: /*[[@{/check_id}]]*/ '/check_id',
type: 'POST',
data: {
loginId: loginId,
registrationType: registrationType,
_csrf: $('input[name="_csrf"]').val()
},
success: function (response) {
handleEmailValidationResponse(response);
},
error: function () {
customPopups.showAlert('처리 중 오류가 발생했습니다. 다시 시도해주세요.');
}
});
});
}); });
</script> </script>
@@ -5,127 +5,94 @@
<th:block th:fragment="newUserForm"> <th:block th:fragment="newUserForm">
<div id="newUserFields"> <div id="newUserFields">
<!-- 성명 --> <!-- 성명 -->
<div class="info1 pt28"> <div class="org-form-group">
<p class="title"> <label class="org-form-label">
<span class="dot">성명</span> <span class="required">*</span>
</p> 성명
<div class="info_line"> </label>
<div class="info_box1"> <div class="org-form-input-wrapper">
<input type="text" th:field="*{userName}" id="userName" class="common_input_type_1" <input type="text" th:field="*{userName}" id="userName" class="org-form-input"
th:placeholder="#{portalUser.Register.userName}"> th:placeholder="#{portalUser.Register.userName}">
</div>
<div class="btn_check pc-only"></div>
</div> </div>
</div> </div>
<!-- 휴대폰 번호 입력 --> <!-- 휴대폰 번호 입력 -->
<div class="info1 pt28"> <div class="org-form-group">
<p class="title"> <label class="org-form-label">
<span class="dot">휴대폰 번호</span> <span class="required">*</span>
</p> 휴대폰 번호
<div class="info_line"> </label>
<div class="info_box1"> <div class="org-form-input-wrapper">
<span class="input"> <div class="org-compound-input">
<div class="custom_select ct_sc h_inp2" id="phoneMobile"> <select class="org-form-select phone-number" id="phoneMobile">
<div class="select_selected">선택</div> <option value="">선택</option>
<ul class="common_selecttype select_items select_hide ct_sc2"> <option value="010">010</option>
<li>010</li> <option value="011">011</option>
<li>011</li> <option value="017">017</option>
<li>017</li> <option value="016">016</option>
<li>016</li> <option value="018">018</option>
<li>018</li> </select>
</ul> <span class="separator">-</span>
</div> <input type="text" id="cellPhone1" maxlength="4" class="org-form-input phone-number"
</span> th:placeholder="#{portal.Register.phone1}">
<span class="dash">-</span> <span class="separator">-</span>
<span class="input"> <input type="text" id="cellPhone2" maxlength="4" class="org-form-input phone-number"
<input type="text" id="cellPhone1" maxlength="4" class="common_input_type_1 w_inp3" th:placeholder="#{portal.Register.phone2}">
th:placeholder="#{portal.Register.phone1}">
</span>
<span class="dash">-</span>
<span class="input">
<input type="text" id="cellPhone2" maxlength="4" class="common_input_type_1 w_inp3"
th:placeholder="#{portal.Register.phone2}">
</span>
<input type="hidden" th:field="*{mobileNumber}" id="mobileNumber">
</div>
<div class="btn_check">
<a class="common_btn_type_2 cbt btn_auth"
th:hx-post="@{/auth/request_auth_number}"
hx-include="#csrf, #mobileNumber"
hx-target="#authNumberContainer"
hx-swap="none">
<span>인증번호 받기</span>
</a>
</div> </div>
<input type="hidden" th:field="*{mobileNumber}" id="mobileNumber">
<button type="button" class="org-btn org-btn-check btn_auth">
인증번호 받기
</button>
<div id="mobile-validation" class="org-validation-message"></div>
</div> </div>
</div> </div>
<div id="mobile-validation"></div>
<!-- 인증번호 입력 --> <!-- 인증번호 입력 -->
<div class="info1 pt28" id="authNumberContainer"> <div class="org-form-group" id="authNumberContainer" style="display: none;">
<p class="title"> <label class="org-form-label">
<span class="dot">인증번호 입력</span> <span class="required">*</span>
</p> 인증번호 입력
<div class="info_line"> </label>
<div class="info_box1"> <div class="org-form-input-wrapper">
<div class="certify_box"> <div class="org-auth-input-group">
<input type="text" th:field="*{authNumber}" id="authNumber" maxlength="6" pattern="[0-9]*" <input type="text" th:field="*{authNumber}" id="authNumber" maxlength="6" pattern="[0-9]*"
inputmode="numeric" class="common_input_type_1 w_input" th:placeholder="#{ncrdRegister.validate.authNumber}" required> inputmode="numeric" class="org-form-input" th:placeholder="#{ncrdRegister.validate.authNumber}" required>
<span class="certify_time" id="certify_time">03:00</span> <span class="org-timer" id="certify_time">03:00</span>
</div>
</div>
<div class="btn_check">
<a class="common_btn_type_2 cbt btn_verify_auth"
th:hx-post="@{/auth/verify_auth_number}"
hx-include="#mobileNumber,#authNumber"
hx-target="#authNumber-validation"
hx-swap="none">
<span>인증번호 확인</span>
</a>
</div> </div>
<button type="button" class="org-btn org-btn-check btn_verify_auth">
인증번호 확인
</button>
<div id="authNumber-validation" class="org-validation-message"></div>
</div> </div>
</div> </div>
<div id="authNumber-validation"></div>
<div class="info1 pt28"> <!-- 비밀번호 -->
<p class="title"> <div class="org-form-group">
<span class="dot">비밀번호</span> <label class="org-form-label">
</p> <span class="required">*</span>
<div class="info_line"> 비밀번호
<div class="info_box1"> </label>
<input type="password" name="password" id="password" class="common_input_type_1 w_input" <div class="org-form-input-wrapper">
th:placeholder="#{portalUser.Register.pass}" <input type="password" name="password" id="password" class="org-form-input"
th:hx-post="@{/check_password}" th:placeholder="#{portalUser.Register.pass}">
hx-trigger="blur" <input type="hidden" name="isPasswordValid" id="isPasswordValid"/>
hx-include="#mobileNumber, #loginId" <div id="password-validation" class="org-validation-message"></div>
hx-target="#password-validation"
hx-swap="innerHTML">
<input type="hidden" name="isPasswordValid" id="isPasswordValid"/>
</div>
<div class="btn_check pc-only"></div>
</div> </div>
</div> </div>
<div id="password-validation" class="validation-message"></div>
<!-- 비밀번호 확인 --> <!-- 비밀번호 확인 -->
<div class="info1 pt28"> <div class="org-form-group">
<p class="title"> <label class="org-form-label">
<span class="dot">비밀번호 확인</span> <span class="required">*</span>
</p> 비밀번호 확인
<div class="info_line"> </label>
<div class="info_box1"> <div class="org-form-input-wrapper">
<input type="password" name="password2" id="password2" class="common_input_type_1 w_input" <input type="password" name="password2" id="password2" class="org-form-input"
th:placeholder="#{portalUser.Register.passConfirm}" th:placeholder="#{portalUser.Register.passConfirm}">
th:hx-post="@{/check_password_match}" <input type="hidden" name="isPasswordMatch" id="isPasswordMatch"/>
hx-trigger="blur" <div id="password-match-validation" class="org-validation-message"></div>
hx-target="#password-match-validation"
hx-include="#password"
hx-swap="innerHTML">
<input type="hidden" name="isPasswordMatch" id="isPasswordMatch"/>
</div>
<div class="btn_check pc-only"></div>
</div> </div>
</div> </div>
<div id="password-match-validation" class="validation-message"></div>
</div> </div>
</th:block> </th:block>
@@ -134,16 +101,16 @@
<script th:fragment="newUserScript"> <script th:fragment="newUserScript">
// 휴대폰 번호 조합 함수 // 휴대폰 번호 조합 함수
function combineMobileNumber() { function combineMobileNumber() {
let mobileElement = document.querySelector('#phoneMobile .select_selected'); let mobileSelect = document.getElementById('phoneMobile');
if (!mobileElement) { if (!mobileSelect) {
console.error('Mobile element not found'); console.error('Mobile select element not found');
return null; return null;
} }
let prefix = mobileElement.textContent.trim(); let prefix = mobileSelect.value;
let middle = document.getElementById('cellPhone1').value.trim(); let middle = document.getElementById('cellPhone1').value.trim();
let last = document.getElementById('cellPhone2').value.trim(); let last = document.getElementById('cellPhone2').value.trim();
if (prefix === '선택' || !middle || !last) { if (!prefix || !middle || !last) {
return null; return null;
} }
@@ -159,7 +126,6 @@
} }
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
activateSelect();
let countdownInterval; let countdownInterval;
let isAuthVerified = false; let isAuthVerified = false;
@@ -182,11 +148,9 @@
combineMobileNumber(); combineMobileNumber();
}); });
// select_selected 변경 감지 // select 변경 감지
$('#phoneMobile .select_items li').on('click', function () { $('#phoneMobile').on('change', function () {
setTimeout(() => { combineMobileNumber();
combineMobileNumber();
}, 100);
}); });
// 인증번호 입력 제한 // 인증번호 입력 제한
@@ -215,8 +179,10 @@
function handleAuthNumberRequest(response) { function handleAuthNumberRequest(response) {
if (response.valid) { if (response.valid) {
let authButton = document.querySelector(".btn_auth"); let authButton = document.querySelector(".btn_auth");
let authContainer = document.getElementById("authNumberContainer");
if (authButton) authButton.disabled = true; if (authButton) authButton.disabled = true;
if (authContainer) authContainer.style.display = 'flex';
startAuthNumberTimer(); startAuthNumberTimer();
customPopups.showAlert(response.message || "인증번호가 발송되었습니다."); customPopups.showAlert(response.message || "인증번호가 발송되었습니다.");
} else { } else {
@@ -256,7 +222,8 @@
} }
// 이벤트 리스너 등록 // 이벤트 리스너 등록
document.querySelectorAll('#phoneMobile .select_selected, #cellPhone1, #cellPhone2') document.getElementById('phoneMobile').addEventListener('change', combineMobileNumber);
document.querySelectorAll('#cellPhone1, #cellPhone2')
.forEach(el => el.addEventListener('input', combineMobileNumber)); .forEach(el => el.addEventListener('input', combineMobileNumber));
let authNumberInput = document.getElementById("authNumber"); let authNumberInput = document.getElementById("authNumber");
@@ -275,76 +242,107 @@
} }
// 인증번호 요청 버튼 이벤트 리스너 // 인증번호 요청 버튼 이벤트 리스너
let authButton = document.querySelector(".btn_auth"); $('.btn_auth').on('click', function () {
if (authButton) { let mobileNumber = combineMobileNumber();
authButton.addEventListener("click", function (event) { if (!mobileNumber) {
let mobileNumber = combineMobileNumber(); customPopups.showAlert("휴대폰 번호를 올바르게 입력해주세요.");
if (!mobileNumber) { return;
event.preventDefault(); // htmx 요청 중지
customPopups.showAlert("휴대폰 번호를 올바르게 입력해주세요.");
return;
}
document.getElementById('mobileNumber').value = mobileNumber;
// htmx 요청이 자동으로 실행됨
});
}
// 인증번호 확인 버튼 이벤트 리스너
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;
}
});
}
// HTMX 이벤트 리스너
document.body.addEventListener('htmx:afterSwap', function (event) {
let targetId = event.detail.target ? event.detail.target.id : null;
if (targetId && ['password-validation', 'password-match-validation', 'authNumberContainer', 'authNumber-validation', 'email-validation'].includes(targetId)) {
let response;
try {
response = typeof event.detail.xhr.response === 'string'
? JSON.parse(event.detail.xhr.response)
: event.detail.xhr.response;
} catch (e) {
console.error('Failed to parse response as JSON:', event.detail.xhr.response);
customPopups.showAlert("서버에서 예상치 못한 응답을 받았습니다.");
return;
}
switch (targetId) {
case 'email-validation':
// handleEmailValidationResponse(response);
break;
case 'password-validation':
case 'password-match-validation':
let targetElement = document.getElementById(targetId);
targetElement.className = 'validation-message ' + (response.valid ? 'success' : 'error');
targetElement.textContent = response.message;
// 유효성 상태 저장
if (targetId === 'password-validation') {
document.getElementById('isPasswordValid').value = response.valid;
} else if (targetId === 'password-match-validation') {
document.getElementById('isPasswordMatch').value = response.valid;
}
break;
case 'authNumberContainer':
handleAuthNumberRequest(response);
break;
case 'authNumber-validation':
handleAuthNumberVerification(response);
break;
}
} }
$('#mobileNumber').val(mobileNumber);
$.ajax({
url: /*[[@{/auth/request_auth_number}]]*/ '/auth/request_auth_number',
type: 'POST',
data: {
mobileNumber: mobileNumber,
_csrf: $('input[name="_csrf"]').val()
},
success: function (response) {
handleAuthNumberRequest(response);
},
error: function () {
customPopups.showAlert("처리 중 오류가 발생했습니다. 다시 시도해주세요.");
}
});
}); });
document.body.addEventListener('htmx:responseError', function (event) { // 인증번호 확인 버튼 이벤트 리스너
customPopups.showAlert("처리 중 오류가 발생했습니다. 다시 시도해주세요."); $('.btn_verify_auth').on('click', function () {
let authNumber = $('#authNumber').val();
if (authNumber.length !== 6) {
customPopups.showAlert("6자리 인증번호를 입력해주세요.");
return;
}
$.ajax({
url: /*[[@{/auth/verify_auth_number}]]*/ '/auth/verify_auth_number',
type: 'POST',
data: {
mobileNumber: $('#mobileNumber').val(),
authNumber: authNumber,
_csrf: $('input[name="_csrf"]').val()
},
success: function (response) {
handleAuthNumberVerification(response);
},
error: function () {
customPopups.showAlert("처리 중 오류가 발생했습니다. 다시 시도해주세요.");
}
});
});
// 비밀번호 검증
$('#password').on('blur', function () {
let password = $(this).val();
if (!password) return;
$.ajax({
url: /*[[@{/check_password}]]*/ '/check_password',
type: 'POST',
data: {
password: password,
mobileNumber: $('#mobileNumber').val(),
loginId: $('#loginId').val(),
_csrf: $('input[name="_csrf"]').val()
},
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 () {
customPopups.showAlert("처리 중 오류가 발생했습니다. 다시 시도해주세요.");
}
});
});
// 비밀번호 확인 검증
$('#password2').on('blur', function () {
let password2 = $(this).val();
if (!password2) return;
$.ajax({
url: /*[[@{/check_password_match}]]*/ '/check_password_match',
type: 'POST',
data: {
password: $('#password').val(),
password2: password2,
_csrf: $('input[name="_csrf"]').val()
},
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 () {
customPopups.showAlert("처리 중 오류가 발생했습니다. 다시 시도해주세요.");
}
});
}); });
}); });
</script> </script>
@@ -1,287 +1,274 @@
<!doctype html> <!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" <html lang="ko" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:hx-on="http://www.w3.org/1999/xhtml"> xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<body> <body>
<th:block th:fragment="orgInfoForm"> <th:block th:fragment="orgInfoForm">
<div class="form_type"> <!-- Business Registration Number -->
<div class="info1"> <div class="org-form-group">
<p class="title"> <label class="org-form-label">
<span class="dot">사업자등록번호</span> <span class="required">*</span>
</p> 사업자등록번호
<div class="info_line"> </label>
<div class="info_box1"> <div class="org-form-input-wrapper">
<input type="text" class="common_input_type_1 w_inp5 business-number" id="compRegNo1" maxlength="3" <div class="org-compound-input">
th:placeholder="#{portalOrg.Register.compRegNo1}" required> <input type="text" class="org-form-input business-number" id="compRegNo1" maxlength="3"
<span class="dash">-</span> placeholder="000" required>
<input type="text" class="common_input_type_1 w_inp6 business-number" id="compRegNo2" maxlength="2" <span class="separator">-</span>
th:placeholder="#{portalOrg.Register.compRegNo2}" required> <input type="text" class="org-form-input business-number" id="compRegNo2" maxlength="2"
<span class="dash">-</span> placeholder="00" required>
<input type="text" class="common_input_type_1 w_inp7 business-number" id="compRegNo3" maxlength="5" <span class="separator">-</span>
th:placeholder="#{portalOrg.Register.compRegNo3}" required> <input type="text" class="org-form-input business-number" id="compRegNo3" maxlength="5"
</div> placeholder="00000" required>
<div class="btn_check pc-only"></div>
<input type="hidden" id="compRegNo" th:field="*{compRegNo}" hx-trigger="change delay:1s"/>
</div> </div>
<input type="hidden" id="compRegNo" th:field="*{compRegNo}"/>
<div id="compRegNo-validation" class="org-validation-message"></div>
</div> </div>
</div>
<div class="info1 pt28"> <!-- Corporate Registration Number -->
<p class="title"> <div class="org-form-group">
<span class="dot">법인등록번호</span> <label class="org-form-label">
</p> <span class="required">*</span>
<div class="info_line"> 법인등록번호
<div class="info_box1"> </label>
<input type="text" class="common_input_type_1 w_inp8 corp-reg-no" id="corpRegNo1" maxlength="6" <div class="org-form-input-wrapper">
th:placeholder="#{portalOrg.Register.corpRegNo1}" required> <div class="org-compound-input">
<span class="dash">-</span> <input type="text" class="org-form-input corp-reg-no" id="corpRegNo1" maxlength="6"
<input type="text" class="common_input_type_1 w_inp9 corp-reg-no" id="corpRegNo2" maxlength="7" placeholder="000000" required>
th:placeholder="#{portalOrg.Register.corpRegNo2}" required> <span class="separator">-</span>
</div> <input type="text" class="org-form-input corp-reg-no" id="corpRegNo2" maxlength="7"
<div class="btn_check pc-only"></div> placeholder="0000000" required>
<input type="hidden" id="corpRegNo" th:field="*{corpRegNo}" hx-trigger="change delay:1s"/>
</div> </div>
<input type="hidden" id="corpRegNo" th:field="*{corpRegNo}"/>
<div id="corpRegNo-validation" class="org-validation-message"></div>
</div> </div>
</div>
<div class="info1 pt28"> <!-- Company Name -->
<p class="title"> <div class="org-form-group">
<span class="dot">법인명</span> <label class="org-form-label" for="orgName">
</p> <span class="required">*</span>
<div class="info_line"> 법인명
<div class="info_box1"> </label>
<input type="text" class="common_input_type_1" id="orgName" th:field="*{orgName}" <div class="org-form-input-wrapper">
th:placeholder="#{portalOrg.Register.orgName}" <input type="text" class="org-form-input" id="orgName" th:field="*{orgName}"
th:classappend="${#fields.hasErrors('orgName')}? 'is-invalid'" required> placeholder="법인명을 입력하세요"
</div> th:classappend="${#fields.hasErrors('orgName')}? 'is-invalid'" required>
<div class="btn_check pc-only"></div> <div th:if="${#fields.hasErrors('orgName')}" class="org-validation-message error">
<div th:if="${#fields.hasErrors('orgName')}" class="invalid-feedback"> <span th:errors="*{orgName}"></span>
<span th:errors="*{orgName}"></span>
</div>
</div> </div>
</div> </div>
</div>
<div class="info1 pt28"> <!-- CEO Name -->
<p class="title"> <div class="org-form-group">
<span class="dot">대표자 성명</span> <label class="org-form-label" for="ceoName">
</p> <span class="required">*</span>
<div class="info_line"> 대표자 성명
<div class="info_box1"> </label>
<input type="text" class="common_input_type_1" id="ceoName" th:field="*{ceoName}" <div class="org-form-input-wrapper">
th:placeholder="#{portalOrg.Register.ceoName}" <input type="text" class="org-form-input" id="ceoName" th:field="*{ceoName}"
th:classappend="${#fields.hasErrors('ceoName')}? 'is-invalid'" required> placeholder="대표자 성명을 입력하세요"
</div> th:classappend="${#fields.hasErrors('ceoName')}? 'is-invalid'" required>
<div class="btn_check pc-only"></div> <div th:if="${#fields.hasErrors('ceoName')}" class="org-validation-message error">
<div th:if="${#fields.hasErrors('ceoName')}" class="invalid-feedback"> <span th:errors="*{ceoName}"></span>
<span th:errors="*{ceoName}"></span>
</div>
</div> </div>
</div> </div>
</div>
<div class="info1 pt28"> <!-- Business Address -->
<p class="title"> <div class="org-form-group">
<span class="dot">사업장 소재지</span> <label class="org-form-label" for="orgAddr">
</p> <span class="required">*</span>
<div class="info_line"> 사업장 소재지
<div class="info_box1"> </label>
<input type="text" class="common_input_type_1" id="orgAddr" th:field="*{orgAddr}" <div class="org-form-input-wrapper">
th:placeholder="#{portalOrg.Register.orgAddr}" <input type="text" class="org-form-input" id="orgAddr" th:field="*{orgAddr}"
th:classappend="${#fields.hasErrors('orgAddr')}? 'is-invalid'" required> placeholder="사업장 소재지를 입력하세요"
</div> th:classappend="${#fields.hasErrors('orgAddr')}? 'is-invalid'" required>
<div class="btn_check pc-only"></div> <div th:if="${#fields.hasErrors('orgAddr')}" class="org-validation-message error">
<div th:if="${#fields.hasErrors('orgAddr')}" class="invalid-feedback"> <span th:errors="*{orgAddr}"></span>
<span th:errors="*{orgAddr}"></span>
</div>
</div> </div>
</div> </div>
</div>
<div class="info1 pt28"> <!-- Business Type -->
<p class="title"> <div class="org-form-group">
<span class="dot">업태</span> <label class="org-form-label" for="orgSectors">
</p> <span class="required">*</span>
<div class="info_line"> 업태
<div class="info_box1"> </label>
<input type="text" class="common_input_type_1 w_input" id="orgSectors" th:field="*{orgSectors}" <div class="org-form-input-wrapper">
th:placeholder="#{portalOrg.Register.orgSectors}"> <input type="text" class="org-form-input" id="orgSectors" th:field="*{orgSectors}"
</div> placeholder="업태를 입력하세요" required>
<div class="btn_check pc-only"></div>
</div>
</div> </div>
</div>
<div class="info1 pt28"> <!-- Industry Type -->
<p class="title"> <div class="org-form-group">
<span class="dot">업종</span> <label class="org-form-label" for="orgIndustryType">
</p> <span class="required">*</span>
<div class="info_line"> 업종
<div class="info_box1"> </label>
<input type="text" class="common_input_type_1" id="orgIndustryType" th:field="*{orgIndustryType}" <div class="org-form-input-wrapper">
th:placeholder="#{portalOrg.Register.orgIndustryType}"> <input type="text" class="org-form-input" id="orgIndustryType" th:field="*{orgIndustryType}"
</div> placeholder="업종을 입력하세요" required>
<div class="btn_check pc-only"></div>
</div>
</div> </div>
</div>
<div class="info1 pt28"> <!-- Company Phone Number -->
<p class="title"> <div class="org-form-group">
<span class="dot">회사 전화번호</span> <label class="org-form-label">
</p> <span class="required">*</span>
<div class="info_line"> 회사 전화번호
<div class="info_box1"> </label>
<span class="input"> <div class="org-form-input-wrapper">
<div class="custom_select ct_sc h_inp2" id="orgAreaCode"> <div class="org-compound-input">
<div class="select_selected active">선택</div> <select class="org-form-select" id="orgAreaCode" required>
<ul class="common_selecttype select_hide select_items ct_sc2"> <option value="">선택</option>
<li>국번없음</li> <option value="국번없음">국번없음</option>
<li>02</li> <option value="02">02</option>
<li>031</li> <option value="031">031</option>
<li>032</li> <option value="032">032</option>
<li>033</li> <option value="033">033</option>
<li>041</li> <option value="041">041</option>
<li>042</li> <option value="042">042</option>
<li>043</li> <option value="043">043</option>
<li>044</li> <option value="044">044</option>
<li>051</li> <option value="051">051</option>
<li>052</li> <option value="052">052</option>
<li>053</li> <option value="053">053</option>
<li>054</li> <option value="054">054</option>
<li>055</li> <option value="055">055</option>
<li>061</li> <option value="061">061</option>
<li>062</li> <option value="062">062</option>
<li>063</li> <option value="063">063</option>
<li>064</li> <option value="064">064</option>
<li>060</li> <option value="070">070</option>
<li>070</li> <option value="080">080</option>
<li>080</li> <option value="010">010</option>
<li>010</li> <option value="011">011</option>
<li>011</li> <option value="016">016</option>
<li>016</li> <option value="017">017</option>
<li>017</li> <option value="018">018</option>
<li>018</li> <option value="019">019</option>
<li>019</li> </select>
</ul> <span class="separator">-</span>
</div> <input type="text" id="orgPhoneNumber1" class="org-form-input" maxlength="4"
</span> placeholder="0000">
<span class="dash">-</span> <span class="separator">-</span>
<span class="input"> <input type="text" id="orgPhoneNumber2" class="org-form-input" maxlength="4"
<input type="text" id="orgPhoneNumber1" class="common_input_type_1 w_inp3" maxlength="4" placeholder="0000">
th:placeholder="#{portal.Register.phone1}"> </div>
</span> <input type="hidden" id="orgPhoneNumber" th:field="*{orgPhoneNumber}">
<span class="dash">-</span> </div>
<span class="input"> </div>
<input type="text" id="orgPhoneNumber2" class="common_input_type_1 w_inp3" maxlength="4"
th:placeholder="#{portal.Register.phone2}"> <!-- Service Name -->
</span> <div class="org-form-group">
</div> <label class="org-form-label" for="serviceName">
<input type="hidden" id="orgPhoneNumber" th:field="*{orgPhoneNumber}"> <span class="required">*</span>
<div class="btn_check pc-only"></div> 서비스명
</label>
<div class="org-form-input-wrapper">
<input type="text" class="org-form-input" id="serviceName" th:field="*{serviceName}"
placeholder="서비스명을 입력하세요"
th:classappend="${#fields.hasErrors('serviceName')}? 'is-invalid'" required>
<div th:if="${#fields.hasErrors('serviceName')}" class="org-validation-message error">
<span th:errors="*{serviceName}"></span>
</div> </div>
</div> </div>
<div class="info1 pt28"> </div>
<p class="title">
<span class="dot">서비스명</span> <!-- Customer Service Phone Number -->
</p> <div class="org-form-group">
<div class="info_line"> <label class="org-form-label">
<div class="info_box1"> <span class="required">*</span>
<input type="text" class="common_input_type_1" id="serviceName" th:field="*{serviceName}" 고객센터 전화번호
th:placeholder="#{portalOrg.Register.serviceName}" </label>
th:classappend="${#fields.hasErrors('serviceName')}? 'is-invalid'" required> <div class="org-form-input-wrapper">
</div> <div class="org-compound-input">
<div class="btn_check pc-only"></div> <select class="org-form-select" id="scAreaCode" required>
<div th:if="${#fields.hasErrors('serviceName')}" class="invalid-feedback"> <option value="">선택</option>
<span th:errors="*{serviceName}"></span> <option value="국번없음">국번없음</option>
</div> <option value="02">02</option>
<option value="031">031</option>
<option value="032">032</option>
<option value="033">033</option>
<option value="041">041</option>
<option value="042">042</option>
<option value="043">043</option>
<option value="044">044</option>
<option value="051">051</option>
<option value="052">052</option>
<option value="053">053</option>
<option value="054">054</option>
<option value="055">055</option>
<option value="061">061</option>
<option value="062">062</option>
<option value="063">063</option>
<option value="064">064</option>
<option value="070">070</option>
<option value="080">080</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="scPhoneNumber1" class="org-form-input" maxlength="4"
placeholder="0000">
<span class="separator">-</span>
<input type="text" id="scPhoneNumber2" class="org-form-input" maxlength="4"
placeholder="0000">
</div> </div>
<input type="hidden" id="scPhoneNumber" th:field="*{scPhoneNumber}">
</div> </div>
<div class="info1 pt28"> </div>
<p class="title">
<span class="dot">고객센터 전화번호</span> <!-- Business Registration Certificate Upload -->
</p> <div class="org-form-group">
<div class="info_line"> <label class="org-form-label">
<div class="info_box1"> <span class="required">*</span>
<span class="input"> 사업자 등록증
<div class="custom_select ct_sc h_inp2" id="scAreaCode"> </label>
<div class="select_selected active">선택</div> <div class="org-form-input-wrapper">
<ul class="common_selecttype select_hide select_items ct_sc2"> <div class="org-file-upload">
<li>국번없음</li> <input type="text" class="org-file-display" id="fileNameDisplay" readonly placeholder="첨부파일을 선택하세요">
<li>02</li> <input type="hidden" id="compRegFile" th:field="*{compRegFile}">
<li>031</li> <label class="org-file-label" for="files">
<li>032</li> <i class="fas fa-upload"></i> 파일선택
<li>033</li> <input type="file" id="files" name="files"
<li>041</li> accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.hwp,.gif,.jpg,.png">
<li>042</li> </label>
<li>043</li> <button type="button" class="org-file-remove" id="fileRemoveBtn" style="display: none;">
<li>044</li> <i class="fas fa-times"></i>
<li>051</li> </button>
<li>052</li>
<li>053</li>
<li>054</li>
<li>055</li>
<li>061</li>
<li>062</li>
<li>063</li>
<li>064</li>
<li>060</li>
<li>070</li>
<li>080</li>
<li>010</li>
<li>011</li>
<li>016</li>
<li>017</li>
<li>018</li>
<li>019</li>
</ul>
</div>
</span>
<span class="dash">-</span>
<span class="input">
<input type="text" id="scPhoneNumber1" class="common_input_type_1 w_inp3" maxlength="4"
th:placeholder="#{portal.Register.phone1}">
</span>
<span class="dash">-</span>
<span class="input">
<input type="text" id="scPhoneNumber2" class="common_input_type_1 w_inp3" maxlength="4"
th:placeholder="#{portal.Register.phone2}">
</span>
</div>
<input type="hidden" id="scPhoneNumber" th:field="*{scPhoneNumber}">
<div class="btn_check pc-only"></div>
</div> </div>
</div> <div class="org-file-notice">
<div class="info1 pt28"> <p>첨부파일은 10MB 이내로 등록해 주세요.</p>
<p class="title tit_add w_tit"> <p>문서파일과 이미지 파일만 등록 가능합니다. (pdf, doc, docx, xls, xlsx, ppt, pptx, hwp, gif, jpg, png)</p>
<span class="dot">사업자 등록증</span> </div>
</p> <div th:if="${#fields.hasErrors('compRegFile')}" class="org-validation-message error">
<div class="info_line info_add"> <span th:errors="*{compRegFile}"></span>
<div class="info_box1 info_add2">
<div class="filebox">
<input type="text" class="upload-name before" readonly="readonly" placeholder="첨부파일">
<input type="hidden" id="compRegFile" th:field="*{compRegFile}">
</div>
<div class="btn_check">
<div class="file_btn">
<label for="files">파일선택</label>
<input type="file" id="files" name="files" accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.hwp,.gif,.jpg,.png">
</div>
</div>
</div>
<div th:if="${#fields.hasErrors('compRegFile')}" class="invalid-feedback">
<span th:errors="*{compRegFile}"></span>
</div>
<div class="list-dot">
<p>첨부파일은 10MB 이내로 등록해 주세요.</p>
<p>문서파일과 이미지 파일만 등록 가능합니다. (pdf, doc, docx, xls, xlsx, ppt, pptx, hwp, gif, jpg, png)</p>
</div>
</div> </div>
</div> </div>
</div> </div>
</th:block> </th:block>
</body> </body>
<th:block layout:fragment="contentScript"> <th:block layout:fragment="contentScript">
<script th:fragment="orgInfoScript"> <script th:fragment="orgInfoScript">
function validatePhoneNumber(prefix) { function validatePhoneNumber(prefix) {
const areaCode = document.getElementById(`${prefix}AreaCode`).value;
const part1 = document.getElementById(`${prefix}PhoneNumber1`).value.trim(); const part1 = document.getElementById(`${prefix}PhoneNumber1`).value.trim();
const part2 = document.getElementById(`${prefix}PhoneNumber2`).value.trim(); const part2 = document.getElementById(`${prefix}PhoneNumber2`).value.trim();
// 회사 전화번호 검증 // 회사 전화번호 검증
if (!part1 || !part2) { if (!areaCode || areaCode === '' || !part1 || !part2) {
customPopups.showAlert(`${prefix === 'org' ? '회사' : '고객센터'} 전화번호를 모두 입력해주세요.`); customPopups.showAlert(`${prefix === 'org' ? '회사' : '고객센터'} 전화번호를 모두 입력해주세요.`);
return false; return false;
} }
@@ -289,7 +276,6 @@
} }
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
activateSelect();
function checkAndSendData(parts, expectedLengths, hiddenInputId, url, paramName) { function checkAndSendData(parts, expectedLengths, hiddenInputId, url, paramName) {
let isValid = true; let isValid = true;
@@ -305,26 +291,37 @@
if (isValid) { if (isValid) {
document.getElementById(hiddenInputId).value = fullValue; document.getElementById(hiddenInputId).value = fullValue;
let values = {};
values[paramName] = fullValue;
// CSRF 토큰을 정확하게 가져오기 // CSRF 토큰을 정확하게 가져오기
const csrfToken = document.querySelector('input[name="_csrf"]')?.value || const csrfToken = document.querySelector('input[name="_csrf"]')?.value ||
document.querySelector('meta[name="_csrf"]')?.content; document.querySelector('meta[name="_csrf"]')?.content;
htmx.ajax('POST', url, { // AJAX로 검증 요청
values: values, $.ajax({
url: url,
type: 'POST',
data: {
[paramName]: fullValue,
_csrf: csrfToken
},
headers: { headers: {
'X-XSRF-TOKEN': csrfToken,
'X-Custom-Handler': 'registration-validation' 'X-Custom-Handler': 'registration-validation'
}, },
swap: 'none', success: function(response) {
target: '#' + hiddenInputId // 명시적 타겟 지정 const validationDiv = document.getElementById(hiddenInputId + '-validation');
}).then(function() { if (validationDiv) {
// HTMX 이벤트가 제대로 발생하는지 확인 if (response.valid) {
console.log('HTMX request completed'); validationDiv.className = 'org-validation-message success';
}).catch(function(error) { validationDiv.textContent = response.message || '유효한 번호입니다.';
console.error('HTMX request failed:', error); } else {
validationDiv.className = 'org-validation-message error';
validationDiv.textContent = response.message || '유효하지 않은 번호입니다.';
}
}
},
error: function() {
console.error('Validation request failed');
}
}); });
} }
} }
@@ -346,10 +343,11 @@
checkAndSendData(parts, [6, 7], 'corpRegNo', '/check-corp-reg-no', 'corpRegNo'); checkAndSendData(parts, [6, 7], 'corpRegNo', '/check-corp-reg-no', 'corpRegNo');
} }
// 파일 선택 관련 코드 // File upload handling
const fileInput = document.getElementById('files'); const fileInput = document.getElementById('files');
const fileNameInput = document.querySelector('.upload-name'); const fileNameDisplay = document.getElementById('fileNameDisplay');
const compRegFileInput = document.getElementById('compRegFile'); const compRegFileInput = document.getElementById('compRegFile');
const fileRemoveBtn = document.getElementById('fileRemoveBtn');
fileInput.addEventListener('change', function() { fileInput.addEventListener('change', function() {
const files = fileInput.files; const files = fileInput.files;
@@ -378,22 +376,20 @@
return; return;
} }
fileNameInput.value = fileNames.join(', '); fileNameDisplay.value = fileNames.join(', ');
fileNameInput.classList.remove('before'); fileNameDisplay.classList.add('has-file');
fileNameInput.classList.add('i_trash', 'after');
compRegFileInput.value = fileNames.join(', '); // hidden input에 파일 이름들 설정 compRegFileInput.value = fileNames.join(', '); // hidden input에 파일 이름들 설정
fileRemoveBtn.style.display = 'block';
} }
}); });
// 파일명 입력란 클릭 시 파일 제거 // File remove button
fileNameInput.addEventListener('click', function() { fileRemoveBtn.addEventListener('click', function() {
if (fileNameInput.classList.contains('i_trash')) { fileInput.value = '';
fileInput.value = ''; fileNameDisplay.value = '';
fileNameInput.value = ''; compRegFileInput.value = '';
compRegFileInput.value = ''; // hidden input 값도 제거 fileNameDisplay.classList.remove('has-file');
fileNameInput.classList.remove('i_trash', 'after'); fileRemoveBtn.style.display = 'none';
fileNameInput.classList.add('before');
}
}); });
function addListeners() { function addListeners() {
@@ -404,21 +400,15 @@
document.getElementById('corpRegNo1').addEventListener('input', checkCorporateNumber); document.getElementById('corpRegNo1').addEventListener('input', checkCorporateNumber);
document.getElementById('corpRegNo2').addEventListener('input', checkCorporateNumber); document.getElementById('corpRegNo2').addEventListener('input', checkCorporateNumber);
// 전화번호 조합 함수 추가 // 전화번호 조합 함수
function combinePhoneNumber(prefix) { function combinePhoneNumber(prefix) {
const areaCode = document.querySelector(`#${prefix}AreaCode .select_selected`).textContent; const areaCode = document.getElementById(`${prefix}AreaCode`).value;
const part1 = document.getElementById(`${prefix}PhoneNumber1`).value; const part1 = document.getElementById(`${prefix}PhoneNumber1`).value;
const part2 = document.getElementById(`${prefix}PhoneNumber2`).value; const part2 = document.getElementById(`${prefix}PhoneNumber2`).value;
if (areaCode === '선택' || !part1 || !part2) return; if (!areaCode || areaCode === '' || !part1 || !part2) return;
let fullNumber = ''; const fullNumber = `${areaCode}-${part1}-${part2}`;
// 지역번호가 '없음'인 경우에도 접두어로 저장
if (areaCode === '국번없음') {
fullNumber = `국번없음-${part1}-${part2}`;
} else {
fullNumber = `${areaCode}-${part1}-${part2}`;
}
// hidden input에 하이픈이 포함된 값 설정 // hidden input에 하이픈이 포함된 값 설정
const hiddenInput = document.getElementById(`${prefix}PhoneNumber`); const hiddenInput = document.getElementById(`${prefix}PhoneNumber`);
@@ -443,12 +433,11 @@
}); });
// 지역번호 선택 시에도 전화번호 조합 // 지역번호 선택 시에도 전화번호 조합
document.querySelector('#orgAreaCode').addEventListener('click', () => combinePhoneNumber('org')); document.getElementById('orgAreaCode').addEventListener('change', () => combinePhoneNumber('org'));
document.querySelector('#scAreaCode').addEventListener('click', () => combinePhoneNumber('sc')); document.getElementById('scAreaCode').addEventListener('change', () => combinePhoneNumber('sc'));
} }
addListeners(); addListeners();
}); });
</script> </script>
</th:block> </th:block>
@@ -1,181 +1,218 @@
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.w3.org/1999/xhtml"> <html xmlns:th="http://www.thymeleaf.org">
<body> <body>
<th:block th:fragment="orgUserInfoForm"> <th:block th:fragment="orgUserInfoForm">
<div class="info1 pt28"> <!-- Email ID -->
<p class="title"> <div class="org-form-group">
<span class="dot">이메일 아이디</span> <label class="org-form-label">
</p> <span class="required">*</span>
<div class="info_line"> 이메일 아이디
<div class="info_box1"> </label>
<input type="text" id="userId" class="common_input_type_1 w_inp" <div class="org-input-button-group">
th:placeholder="#{portalUser.Register.email}"> <div class="org-compound-input" style="flex: 1;">
<span class="special_t">@</span> <input type="text" id="userId" class="org-form-input"
<input type="text" id="domain" class="common_input_type_1 w_inp2" placeholder="이메일 아이디">
th:placeholder="#{portalUser.Register.domain}"> <span class="separator">@</span>
<input type="hidden" id="loginId" name="loginId" th:value="${loginId}"> <input type="text" id="domain" class="org-form-input"
<input type="hidden" name="registrationType" th:value="${registrationType}"/> placeholder="도메인">
<input type="hidden" id="registrationScenario" name="registrationScenario" value="new"/> </div>
</div> <button type="button" class="org-btn org-btn-check btn_check_email">
<div class="btn_check"> 중복체크
<a class="common_btn_type_2 cbt btn_check_email" </button>
th:hx-post="@{/check_id}"
hx-include="#csrf, #loginId, [name='registrationType']"
hx-target="#email-validation"
hx-swap="none">
<span>중복체크</span>
</a>
</div>
</div>
<div id="email-validation"></div>
</div>
<div id="dynamicFormContainer">
<div id="individualConversionForm" style="display: none;">
<div th:include="~{apps/register/components/individualConversionForm :: individualConversionForm}"></div>
</div>
<div id="emailChangeForm" style="display: none;">
<div th:include="~{apps/register/components/emailChangeForm :: emailChangeForm}"></div>
</div>
<div id="newUserForm" style="display: none;">
<div th:include="~{apps/register/components/newUserInfoForm :: newUserForm}"></div>
</div>
</div> </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>
<!-- Dynamic Form Container -->
<div id="dynamicFormContainer">
<div id="individualConversionForm" style="display: none;">
<div th:include="~{apps/register/components/individualConversionForm :: individualConversionForm}"></div>
</div>
<div id="emailChangeForm" style="display: none;">
<div th:include="~{apps/register/components/emailChangeForm :: emailChangeForm}"></div>
</div>
<div id="newUserForm" style="display: none;">
<div th:include="~{apps/register/components/newUserInfoForm :: newUserForm}"></div>
</div>
</div>
</th:block> </th:block>
</body> </body>
<th:block layout:fragment="contentScript"> <th:block layout:fragment="contentScript">
<script th:fragment="orgUserInfoScript"> <script th:fragment="orgUserInfoScript">
// 이메일 조합 함수 // 이메일 조합 함수
function combineEmail() { function combineEmail() {
let emailId = document.getElementById('userId').value; let emailId = document.getElementById('userId').value;
let domain = document.getElementById('domain').value; let domain = document.getElementById('domain').value;
let combined = emailId + '@' + domain; let combined = emailId + '@' + domain;
let loginIdInput = document.getElementById('loginId'); let loginIdInput = document.getElementById('loginId');
if (loginIdInput) { if (loginIdInput) {
loginIdInput.value = combined; loginIdInput.value = combined;
}
}
function handleEmailValidationResponse(response) {
const individualConversionForm = document.getElementById('individualConversionForm');
const emailChangeForm = document.getElementById('emailChangeForm');
const newUserForm = document.getElementById('newUserForm');
const userIdInput = document.getElementById('userId');
const domainInput = document.getElementById('domain');
const checkButton = document.querySelector('.btn_check_email');
const loginIdInput = document.getElementById('loginId');
const scenarioInput = document.getElementById('registrationScenario');
if (response && response.showPopup) {
switch (response.popupType) {
case "validEmail":
case "newUser":
// 시나리오 값 설정
if (scenarioInput) {
scenarioInput.value = 'new';
// hidden input이 form의 일부인지 확인
if (!scenarioInput.form) {
document.getElementById('registerForm').appendChild(scenarioInput);
}
} }
}
function handleEmailValidationResponse(response) { customPopups.showAlert(response.message);
const individualConversionForm = document.getElementById('individualConversionForm');
const emailChangeForm = document.getElementById('emailChangeForm');
const newUserForm = document.getElementById('newUserForm');
const userIdInput = document.getElementById('userId');
const domainInput = document.getElementById('domain');
const checkButton = document.querySelector('.btn_check_email');
const loginIdInput = document.getElementById('loginId');
const scenarioInput = document.getElementById('registrationScenario');
if (response && response.showPopup) { if (checkButton) {
switch (response.popupType) { checkButton.style.display = 'none';
case "validEmail": }
case "newUser":
// 시나리오 값 설정
if (scenarioInput) {
scenarioInput.value = 'new';
// hidden input이 form의 일부인지 확인
if (!scenarioInput.form) {
document.getElementById('registerForm').appendChild(scenarioInput);
}
}
customPopups.showAlert(response.message); if (userIdInput && domainInput && loginIdInput) {
userIdInput.disabled = true;
domainInput.disabled = true;
userIdInput.style.backgroundColor = '#e0e0e0';
domainInput.style.backgroundColor = '#e0e0e0';
if (checkButton) { const combinedEmail = `${userIdInput.value.trim()}@${domainInput.value.trim()}`;
checkButton.style.display = 'none'; loginIdInput.value = combinedEmail;
} }
if (userIdInput && domainInput && loginIdInput) { if (newUserForm) newUserForm.style.display = 'block';
userIdInput.disabled = true; if (individualConversionForm) individualConversionForm.style.display = 'none';
domainInput.disabled = true; if (emailChangeForm) emailChangeForm.style.display = 'none';
userIdInput.style.backgroundColor = '#e0e0e0'; break;
domainInput.style.backgroundColor = '#e0e0e0';
const combinedEmail = `${userIdInput.value.trim()}@${domainInput.value.trim()}`; case "conversionOrChange":
loginIdInput.value = combinedEmail; customPopups.showEmailValidationPopup(
} response.message,
function () {
if (newUserForm) newUserForm.style.display = 'block'; // "이메일 유지" 선택 시 동작
if (individualConversionForm) individualConversionForm.style.display = 'none'; if (scenarioInput) {
if (emailChangeForm) emailChangeForm.style.display = 'none'; scenarioInput.value = 'retain';
break; if (!scenarioInput.form) {
document.getElementById('registerForm').appendChild(scenarioInput);
case "conversionOrChange": }
customPopups.showEmailValidationPopup(
response.message,
function () {
// "이메일 유지" 선택 시 동작
if (scenarioInput) {
scenarioInput.value = 'retain';
if (!scenarioInput.form) {
document.getElementById('registerForm').appendChild(scenarioInput);
}
}
if (individualConversionForm) individualConversionForm.style.display = 'block';
if (emailChangeForm) emailChangeForm.style.display = 'none';
if (newUserForm) newUserForm.style.display = 'none';
if (userIdInput && domainInput) {
userIdInput.disabled = true;
domainInput.disabled = true;
userIdInput.style.backgroundColor = '#e0e0e0';
domainInput.style.backgroundColor = '#e0e0e0';
}
if (checkButton) {
checkButton.style.display = 'none';
}
},
function () {
// "이메일 전환" 선택 시 동작
if (scenarioInput) {
scenarioInput.value = 'change';
if (!scenarioInput.form) {
document.getElementById('registerForm').appendChild(scenarioInput);
}
}
if (emailChangeForm) emailChangeForm.style.display = 'block';
if (individualConversionForm) individualConversionForm.style.display = 'none';
if (newUserForm) newUserForm.style.display = 'none';
if (userIdInput && domainInput) {
userIdInput.disabled = true;
domainInput.disabled = true;
userIdInput.style.backgroundColor = '#e0e0e0';
domainInput.style.backgroundColor = '#e0e0e0';
}
if (checkButton) {
checkButton.style.display = 'none';
}
}
);
break;
case "error":
customPopups.showAlert(response.message);
break;
default:
console.error('알 수 없는 팝업 유형:', response.popupType);
break;
} }
} else if (response && response.message) {
customPopups.showAlert(response.message); if (individualConversionForm) individualConversionForm.style.display = 'block';
} else { if (emailChangeForm) emailChangeForm.style.display = 'none';
customPopups.showAlert('알 수 없는 오류가 발생했습니다.'); if (newUserForm) newUserForm.style.display = 'none';
}
if (userIdInput && domainInput) {
userIdInput.disabled = true;
domainInput.disabled = true;
userIdInput.style.backgroundColor = '#e0e0e0';
domainInput.style.backgroundColor = '#e0e0e0';
}
if (checkButton) {
checkButton.style.display = 'none';
}
},
function () {
// "이메일 전환" 선택 시 동작
if (scenarioInput) {
scenarioInput.value = 'change';
if (!scenarioInput.form) {
document.getElementById('registerForm').appendChild(scenarioInput);
}
}
if (emailChangeForm) emailChangeForm.style.display = 'block';
if (individualConversionForm) individualConversionForm.style.display = 'none';
if (newUserForm) newUserForm.style.display = 'none';
if (userIdInput && domainInput) {
userIdInput.disabled = true;
domainInput.disabled = true;
userIdInput.style.backgroundColor = '#e0e0e0';
domainInput.style.backgroundColor = '#e0e0e0';
}
if (checkButton) {
checkButton.style.display = 'none';
}
}
);
break;
case "error":
customPopups.showAlert(response.message);
break;
default:
console.error('알 수 없는 팝업 유형:', response.popupType);
break;
} }
} else if (response && response.message) {
customPopups.showAlert(response.message);
} else {
customPopups.showAlert('알 수 없는 오류가 발생했습니다.');
}
}
// 이메일 입력 필드에 이벤트 리스너 추가 // Email check button click handler
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
let userIdInput = document.getElementById("userId"); let userIdInput = document.getElementById("userId");
let domainInput = document.getElementById("domain"); let domainInput = document.getElementById("domain");
if (userIdInput) userIdInput.addEventListener("input", combineEmail); const checkButton = document.querySelector('.btn_check_email');
if (domainInput) domainInput.addEventListener("input", combineEmail);
if (userIdInput) userIdInput.addEventListener("input", combineEmail);
if (domainInput) domainInput.addEventListener("input", combineEmail);
// Email check button with AJAX
if (checkButton) {
checkButton.addEventListener('click', function() {
const loginId = document.getElementById('loginId').value;
const registrationType = document.querySelector('input[name="registrationType"]').value;
if (!loginId || loginId.indexOf('@') === -1) {
customPopups.showAlert('이메일 주소를 입력해주세요.');
return;
}
const csrfToken = document.querySelector('input[name="_csrf"]')?.value;
// Show loading
document.getElementById('loadingOverlay')?.classList.add('active');
$.ajax({
url: '/check_id',
type: 'POST',
data: {
loginId: loginId,
registrationType: registrationType,
_csrf: csrfToken
},
success: function(response) {
document.getElementById('loadingOverlay')?.classList.remove('active');
handleEmailValidationResponse(response);
},
error: function() {
document.getElementById('loadingOverlay')?.classList.remove('active');
customPopups.showAlert('서버 오류가 발생했습니다. 다시 시도해주세요.');
}
});
}); });
}
});
</script> </script>
</th:block> </th:block>
</html> </html>
@@ -1,501 +1,515 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" <html lang="ko" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/kbank_base_layout}"> layout:decorate="~{layout/kjbank_signup_layout}">
<body> <body>
<section layout:fragment="contentFragment" class="content"> <section layout:fragment="title">
<script th:src="@{/js/htmx.min.js}"></script> <div class="org-page-title-banner">
<div class="content_wrap"> <img th:src="@{/img/user_register_title.png}" alt="법인회원가입" class="title-image">
<div class="sub_title2"> <h1>법인회원가입</h1>
<h2 class="title add">법인회원가입</h2> </div>
</div>
<div class="inner i_cs h_inner2">
<!-- 약관동의 섹션 -->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<input type="hidden" name="registrationType" th:value="${registrationType}"/>
<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">
<!-- 법인 기본 정보 -->
<div class="terms_box">
<p>법인 기본 정보</p>
<span class="dot">필수 입력</span>
</div>
<div class="form_type">
<th:block th:replace="~{apps/register/components/orgBasicInfoForm :: orgInfoForm}"></th:block>
</div>
<!-- 기본 관리자 정보 -->
<div class="terms_box">
<p>법인 관리자 정보</p>
<span class="dot">필수 입력</span>
</div>
<div class="top_info mtb12">
<ul>
<li>법인관리자는 API 신청 및 이용자 초대 등 모든 권한을 보유합니다.</li>
</ul>
</div>
<div class="form_type" th:if="${registrationType == 'corporate'}">
<th:block th:replace="~{apps/register/components/orgUserInfoForm :: orgUserInfoForm}"></th:block>
</div>
<!-- 가입 버튼 -->
<div class="form_type">
<div class="btn_application">
<a type="button" class="common_btn_type_1 gray" id="cancelButton"><span>취소</span></a>
<div class="btn_gap"></div>
<a type="button" class="common_btn_type_1 btn_register"><span>가입신청</span></a>
</div>
</div>
</form>
</div>
</div>
</section> </section>
<section layout:fragment="contentFragment">
<div class="org-register-page">
<!-- Page Title Banner -->
<div class="org-register-container">
<!-- Hidden Fields -->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<input type="hidden" name="registrationType" th:value="${registrationType}"/>
<!-- Alert Container -->
<div id="alertContainer"></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" style="margin-top: 48px;">
<h3>법인 관리자 정보</h3>
<span class="required-badge">필수 입력</span>
</div>
<div class="org-info-notice">
<ul>
<li>법인관리자는 API 신청 및 이용자 초대 등 모든 권한을 보유합니다.</li>
</ul>
</div>
<div th:if="${registrationType == 'corporate'}">
<th:block th:replace="~{apps/register/components/orgUserInfoForm :: orgUserInfoForm}"></th:block>
</div>
<!-- Action Buttons -->
<div class="org-action-buttons">
<button type="button" class="org-btn org-btn-secondary" id="cancelButton">취소</button>
<button type="button" class="org-btn org-btn-primary btn_register">가입신청</button>
</div>
</form>
<!-- Loading Overlay -->
<div class="org-loading-overlay" id="loadingOverlay">
<div class="spinner"></div>
</div>
</div>
</div>
</section>
<th:block layout:fragment="contentScript"> <th:block layout:fragment="contentScript">
<script th:if="${error}" th:inline="javascript"> <script th:if="${error}" th:inline="javascript">
$(document).ready(function () { $(document).ready(function () {
customPopups.showAlert([[${error}]]); customPopups.showAlert([[${error}]]);
});
</script>
<script th:replace="~{apps/register/userAgreementContent :: agreementScript}"></script>
<script th:replace="~{apps/register/components/orgBasicInfoForm :: orgInfoScript}"></script>
<script th:replace="~{apps/register/components/orgUserInfoForm :: orgUserInfoScript}"></script>
<script th:replace="~{apps/register/components/emailChangeForm :: emailChangeScript}"></script>
<script th:replace="~{apps/register/components/newUserInfoForm :: newUserScript}"></script>
<script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function () {
let form = document.getElementById('registerForm');
let agreementForm = document.getElementById('agreementForm');
let btn_register = document.querySelector('.btn_register');
let isAuthVerified = false;
// registrationScenario를 hidden input에서 가져오도록 수정
let registrationScenario = document.getElementById('registrationScenario')?.value || 'new';
// hidden input의 값이 변경되면 registrationScenario 변수도 업데이트
const scenarioInput = document.getElementById('registrationScenario');
if (scenarioInput) {
scenarioInput.addEventListener('change', function () {
registrationScenario = this.value;
console.log('Registration Scenario Updated:', registrationScenario);
}); });
</script>
<script th:replace="~{apps/register/userAgreementContent :: agreementScript}"></script>
<script th:replace="~{apps/register/components/orgBasicInfoForm :: orgInfoScript}"></script>
<script th:replace="~{apps/register/components/orgUserInfoForm :: orgUserInfoScript}"></script>
<script th:replace="~{apps/register/components/emailChangeForm :: emailChangeScript}"></script>
<script th:replace="~{apps/register/components/newUserInfoForm :: newUserScript}"></script>
<script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function () {
activateSelect();
let form = document.getElementById('registerForm');
let agreementForm = document.getElementById('agreementForm');
let btn_register = document.querySelector('.btn_register');
let isAuthVerified = false;
// registrationScenario를 hidden input에서 가져오도록 수정 // MutationObserver를 사용하여 값 변경 감지
let registrationScenario = document.getElementById('registrationScenario')?.value || 'new'; const observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
// hidden input의 값이 변경되면 registrationScenario 변수도 업데이트 if (mutation.type === 'attributes' && mutation.attributeName === 'value') {
const scenarioInput = document.getElementById('registrationScenario'); registrationScenario = scenarioInput.value;
if (scenarioInput) { console.log('Registration Scenario Updated (from mutation):', registrationScenario);
scenarioInput.addEventListener('change', function () {
registrationScenario = this.value;
console.log('Registration Scenario Updated:', registrationScenario);
});
// MutationObserver를 사용하여 값 변경 감지
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.type === 'attributes' && mutation.attributeName === 'value') {
registrationScenario = scenarioInput.value;
console.log('Registration Scenario Updated (from mutation):', registrationScenario);
}
});
});
observer.observe(scenarioInput, {attributes: true});
} }
});
});
observer.observe(scenarioInput, {attributes: true});
}
// 시나리오별 필수 필드 정의 // 시나리오별 필수 필드 정의
const requiredFieldsByScenario = { const requiredFieldsByScenario = {
new: { new: {
user: ['loginId', 'userName', 'password', 'password2', 'mobileNumber', 'authNumber'], user: ['loginId', 'userName', 'password', 'password2', 'mobileNumber', 'authNumber'],
org: ['compRegNo', 'corpRegNo', 'orgName', 'ceoName', 'orgAddr', 'orgSectors', 'orgIndustryType', 'orgPhoneNumber', 'scPhoneNumber', 'serviceName', 'compRegFile', 'files'] org: ['compRegNo', 'corpRegNo', 'orgName', 'ceoName', 'orgAddr', 'orgSectors', 'orgIndustryType', 'orgPhoneNumber', 'scPhoneNumber', 'serviceName', 'compRegFile', 'files']
}, },
retain: { retain: {
user: ['loginId', 'passwordConfirmIndividual'], user: ['loginId', 'passwordConfirmIndividual'],
org: ['compRegNo', 'corpRegNo', 'orgName', 'ceoName', 'orgAddr', 'orgSectors', 'orgIndustryType', 'orgPhoneNumber', 'scPhoneNumber', 'serviceName', 'compRegFile', 'files'] org: ['compRegNo', 'corpRegNo', 'orgName', 'ceoName', 'orgAddr', 'orgSectors', 'orgIndustryType', 'orgPhoneNumber', 'scPhoneNumber', 'serviceName', 'compRegFile', 'files']
}, },
change: { change: {
user: ['loginId', 'passwordConfirmEmailChange', 'newLoginId'], user: ['loginId', 'passwordConfirmEmailChange', 'newLoginId'],
org: ['compRegNo', 'corpRegNo', 'orgName', 'ceoName', 'orgAddr', 'orgSectors', 'orgIndustryType', 'orgPhoneNumber', 'scPhoneNumber', 'serviceName', 'compRegFile', 'files'] org: ['compRegNo', 'corpRegNo', 'orgName', 'ceoName', 'orgAddr', 'orgSectors', 'orgIndustryType', 'orgPhoneNumber', 'scPhoneNumber', 'serviceName', 'compRegFile', 'files']
} }
}; };
// 이메일 유효성 검사 함수 // 이메일 유효성 검사 함수
function isValidEmail(email) { function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email); return emailRegex.test(email);
}
function collectFormData() {
const form = document.createElement('form');
form.enctype = 'multipart/form-data';
form.method = 'POST';
form.action = '/signup/portalOrg';
form.style.display = 'none';
const fields = requiredFieldsByScenario[registrationScenario];
if (!fields) {
console.error('Unknown scenario');
return form;
}
// 시나리오 값 추가
const scenarioInput = document.getElementById('registrationScenario');
if (scenarioInput) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'registrationScenario';
input.value = scenarioInput.value;
form.appendChild(input);
} else {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'registrationScenario';
input.value = registrationScenario;
form.appendChild(input);
}
// user와 org 필드를 따로 처리
fields.user.forEach(fieldId => {
const element = document.getElementById(fieldId);
if (element) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = fieldId;
input.value = element.value;
form.appendChild(input);
}
});
fields.org.forEach(fieldId => {
const element = document.getElementById(fieldId);
if (element) {
if (element.type === 'file') {
const files = element.files;
if (files.length > 0) {
const input = document.createElement('input');
input.type = 'file';
input.name = fieldId;
input.files = files;
form.appendChild(input);
}
} else {
const input = document.createElement('input');
input.type = 'hidden';
input.name = fieldId;
input.value = element.value;
form.appendChild(input);
} }
}
});
function collectFormData() { // Add confirmPassword manually based on the scenario
const form = document.createElement('form'); if (registrationScenario === 'retain') {
form.enctype = 'multipart/form-data'; const passwordConfirmIndividual = document.getElementById('passwordConfirmIndividual');
form.method = 'POST'; if (passwordConfirmIndividual) {
form.action = '/signup/portalOrg'; const input = document.createElement('input');
form.style.display = 'none'; input.type = 'hidden';
input.name = 'confirmPassword';
input.value = passwordConfirmIndividual.value;
form.appendChild(input);
}
} else if (registrationScenario === 'change') {
const passwordConfirmEmailChange = document.getElementById('passwordConfirmEmailChange');
if (passwordConfirmEmailChange) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'confirmPassword';
input.value = passwordConfirmEmailChange.value;
form.appendChild(input);
}
}
const fields = requiredFieldsByScenario[registrationScenario]; // 공통 필드 추가
if (document.getElementById('termsOfUse')) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'termsOfUse';
input.value = document.getElementById('termsOfUse').checked;
form.appendChild(input);
}
if (!fields) { if (document.getElementById('privacyCollect')) {
console.error('Unknown scenario'); const input = document.createElement('input');
return form; input.type = 'hidden';
} input.name = 'privacyCollect';
input.value = document.getElementById('privacyCollect').checked;
form.appendChild(input);
}
// 시나리오 값 추가 // 시스템 필드 추가
const scenarioInput = document.getElementById('registrationScenario'); const csrfToken = document.querySelector('input[name="_csrf"]');
if (scenarioInput) { if (csrfToken) {
const input = document.createElement('input'); const input = document.createElement('input');
input.type = 'hidden'; input.type = 'hidden';
input.name = 'registrationScenario'; input.name = '_csrf';
input.value = scenarioInput.value; input.value = csrfToken.value;
form.appendChild(input); form.appendChild(input);
} else { }
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'registrationScenario';
input.value = registrationScenario;
form.appendChild(input);
}
// user와 org 필드를 따로 처리 const registrationType = document.querySelector('input[name="registrationType"]');
fields.user.forEach(fieldId => { if (registrationType) {
const element = document.getElementById(fieldId); const input = document.createElement('input');
if (element) { input.type = 'hidden';
const input = document.createElement('input'); input.name = 'registrationType';
input.type = 'hidden'; input.value = registrationType.value;
input.name = fieldId; form.appendChild(input);
input.value = element.value; }
form.appendChild(input);
}
});
fields.org.forEach(fieldId => { console.log('Collected Form Data for scenario:',
const element = document.getElementById(fieldId); document.getElementById('registrationScenario')?.value || registrationScenario);
if (element) { return form;
if (element.type === 'file') { }
const files = element.files;
if (files.length > 0) {
const input = document.createElement('input');
input.type = 'file';
input.name = fieldId;
input.files = files;
form.appendChild(input);
}
} else {
const input = document.createElement('input');
input.type = 'hidden';
input.name = fieldId;
input.value = element.value;
form.appendChild(input);
}
}
});
// Add confirmPassword manually based on the scenario // 시나리오별 유효성 검사
if (registrationScenario === 'retain') { function validateFormByScenario() {
const passwordConfirmIndividual = document.getElementById('passwordConfirmIndividual');
if (passwordConfirmIndividual) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'confirmPassword';
input.value = passwordConfirmIndividual.value;
form.appendChild(input);
}
} else if (registrationScenario === 'change') {
const passwordConfirmEmailChange = document.getElementById('passwordConfirmEmailChange');
if (passwordConfirmEmailChange) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'confirmPassword';
input.value = passwordConfirmEmailChange.value;
form.appendChild(input);
}
}
// 공통 필드 추가 const currentScenario = document.getElementById('registrationScenario')?.value || 'new';
if (document.getElementById('termsOfUse')) { console.log('Registration Scenario:', currentScenario);
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'termsOfUse';
input.value = document.getElementById('termsOfUse').checked;
form.appendChild(input);
}
if (document.getElementById('privacyCollect')) { if (!currentScenario) {
const input = document.createElement('input'); console.log('Unknown scenario');
input.type = 'hidden'; return false;
input.name = 'privacyCollect'; }
input.value = document.getElementById('privacyCollect').checked;
form.appendChild(input);
}
// 시스템 필드 추가 switch (registrationScenario) {
const csrfToken = document.querySelector('input[name="_csrf"]'); case 'new':
if (csrfToken) { const loginIdElement = document.getElementById('loginId');
const input = document.createElement('input'); const userNameElement = document.getElementById('userName');
input.type = 'hidden';
input.name = '_csrf';
input.value = csrfToken.value;
form.appendChild(input);
}
const registrationType = document.querySelector('input[name="registrationType"]'); const isEmailValid = loginIdElement ? isValidEmail(loginIdElement.value) : false;
if (registrationType) { const hasUserName = userNameElement ? userNameElement.value.trim() !== '' : false;
const input = document.createElement('input'); const isPasswordValid = document.getElementById('isPasswordValid').value.trim() === 'true';
input.type = 'hidden'; const isPasswordMatch = document.getElementById('isPasswordMatch').value.trim() === 'true';
input.name = 'registrationType';
input.value = registrationType.value;
form.appendChild(input);
}
console.log('Collected Form Data for scenario:', console.log('New User Validation:', {
document.getElementById('registrationScenario')?.value || registrationScenario); email: loginIdElement?.value,
return form; isEmailValid,
} isPasswordValid,
isPasswordMatch,
userName: userNameElement?.value,
hasUserName,
isAuthVerified,
requiredFields: currentScenario
});
return isEmailValid && isPasswordValid && isPasswordMatch && hasUserName && isAuthVerified;
// 시나리오별 유효성 검사 case 'retain':
function validateFormByScenario() { const passwordConfirmElement = document.getElementById('passwordConfirmIndividual');
const hasPassword = passwordConfirmElement ? passwordConfirmElement.value.length >= 8 : false;
const retainLoginIdElement = document.getElementById('loginId');
const currentScenario = document.getElementById('registrationScenario')?.value || 'new'; console.log('Retain User Validation:', {
console.log('Registration Scenario:', currentScenario); email: retainLoginIdElement?.value,
isEmailValid: retainLoginIdElement ? isValidEmail(retainLoginIdElement.value) : false,
passwordLength: passwordConfirmElement?.value.length,
hasPassword,
requiredFields: currentScenario
});
return retainLoginIdElement && isValidEmail(retainLoginIdElement.value) && hasPassword;
if (!currentScenario) { case 'change':
console.log('Unknown scenario'); const changeLoginIdElement = document.getElementById('loginId');
return false; const newLoginIdElement = document.getElementById('newLoginId');
} const changePasswordElement = document.getElementById('passwordConfirmEmailChange');
const hasChangePassword = changePasswordElement ? changePasswordElement.value.length >= 8 : false;
switch (registrationScenario) { console.log('Change Email Validation:', {
case 'new': currentEmail: changeLoginIdElement?.value,
const loginIdElement = document.getElementById('loginId'); newEmail: newLoginIdElement?.value,
const userNameElement = document.getElementById('userName'); hasPassword: hasChangePassword,
requiredFields: currentScenario
});
return changeLoginIdElement && newLoginIdElement &&
isValidEmail(changeLoginIdElement.value) &&
hasChangePassword &&
isValidEmail(newLoginIdElement.value);
const isEmailValid = loginIdElement ? isValidEmail(loginIdElement.value) : false; default:
const hasUserName = userNameElement ? userNameElement.value.trim() !== '' : false; console.log('Unknown scenario');
const isPasswordValid = document.getElementById('isPasswordValid').value.trim() === 'true'; return false;
const isPasswordMatch = document.getElementById('isPasswordMatch').value.trim() === 'true'; }
}
console.log('New User Validation:', { // 공통 필드 유효성 검사
email: loginIdElement?.value, function validateCommonFields() {
isEmailValid,
isPasswordValid,
isPasswordMatch,
userName: userNameElement?.value,
hasUserName,
isAuthVerified,
requiredFields: currentScenario
});
return isEmailValid && isPasswordValid && isPasswordMatch && hasUserName && isAuthVerified;
case 'retain': if(!validatePhoneNumber('org') || !validatePhoneNumber('sc')) {
const passwordConfirmElement = document.getElementById('passwordConfirmIndividual'); return false;
const hasPassword = passwordConfirmElement ? passwordConfirmElement.value.length >= 8 : false; }
const retainLoginIdElement = document.getElementById('loginId');
console.log('Retain User Validation:', { const validations = {
email: retainLoginIdElement?.value, termsOfUse: $('#termsOfUse').prop('checked'),
isEmailValid: retainLoginIdElement ? isValidEmail(retainLoginIdElement.value) : false, privacyPolicy: $('#privacyCollect').prop('checked'),
passwordLength: passwordConfirmElement?.value.length, compRegNo: $('#compRegNo').val().trim() !== '',
hasPassword, corpRegNo: $('#corpRegNo').val().trim() !== '',
requiredFields: currentScenario orgName: $('#orgName').val().trim() !== '',
}); ceoName: $('#ceoName').val().trim() !== '',
return retainLoginIdElement && isValidEmail(retainLoginIdElement.value) && hasPassword; orgAddr: $('#orgAddr').val().trim() !== '',
orgPhoneNumber: $('#orgPhoneNumber').val().trim() !== '',
scPhoneNumber: $('#scPhoneNumber').val().trim() !== '',
serviceName: $('#serviceName').val().trim() !== '',
compRegFile: $('#compRegFile').val().trim() !== ''
};
case 'change': console.log('Common Fields Validation:', validations);
const changeLoginIdElement = document.getElementById('loginId'); return Object.values(validations).every(value => value === true);
const newLoginIdElement = document.getElementById('newLoginId'); }
const changePasswordElement = document.getElementById('passwordConfirmEmailChange');
const hasChangePassword = changePasswordElement ? changePasswordElement.value.length >= 8 : false;
console.log('Change Email Validation:', { // 전체 폼 유효성 검사
currentEmail: changeLoginIdElement?.value, function validateForm() {
newEmail: newLoginIdElement?.value, const scenarioValidation = validateFormByScenario();
hasPassword: hasChangePassword, const commonValidation = validateCommonFields();
requiredFields: currentScenario
});
return changeLoginIdElement && newLoginIdElement &&
isValidEmail(changeLoginIdElement.value) &&
hasChangePassword &&
isValidEmail(newLoginIdElement.value);
default: console.log('Form validation result:', {
console.log('Unknown scenario'); scenarioValidation,
return false; commonValidation,
} registrationScenario
} });
// 공통 필드 유효성 검사 return scenarioValidation && commonValidation;
function validateCommonFields() { }
if(!validatePhoneNumber('org') || !validatePhoneNumber('sc')) { let isSubmitting = false;
return false;
}
const validations = { // 등록 버튼 클릭 이벤트
termsOfUse: $('#termsOfUse').prop('checked'), btn_register.addEventListener('click', function (event) {
privacyPolicy: $('#privacyCollect').prop('checked'), event.preventDefault();
compRegNo: $('#compRegNo').val().trim() !== '', event.stopPropagation();
corpRegNo: $('#corpRegNo').val().trim() !== '',
orgName: $('#orgName').val().trim() !== '',
ceoName: $('#ceoName').val().trim() !== '',
orgAddr: $('#orgAddr').val().trim() !== '',
orgPhoneNumber: $('#orgPhoneNumber').val().trim() !== '',
scPhoneNumber: $('#scPhoneNumber').val().trim() !== '',
serviceName: $('#serviceName').val().trim() !== '',
compRegFile: $('#compRegFile').val().trim() !== ''
};
console.log('Common Fields Validation:', validations); // Prevent resubmission
return Object.values(validations).every(value => value === true); if (isSubmitting) {
} console.log('Form is already being submitted...');
return;
}
// 전체 폼 유효성 검사 isSubmitting = true;
function validateForm() { btn_register.disabled = true;
const scenarioValidation = validateFormByScenario();
const commonValidation = validateCommonFields();
console.log('Form validation result:', { // Show loading overlay
scenarioValidation, document.getElementById('loadingOverlay').classList.add('active');
commonValidation,
registrationScenario
});
return scenarioValidation && commonValidation; try {
} form.classList.add('was-validated');
if (agreementForm) {
agreementForm.classList.add('was-validated');
}
let isSubmitting = false; console.log('Validating form...');
// 등록 버튼 클릭 이벤트 if (validateForm()) {
btn_register.addEventListener('click', function (event) { console.log('Form validation passed, collecting form data...');
event.preventDefault(); const form = collectFormData();
event.stopPropagation(); document.body.appendChild(form);
form.submit();
} else {
console.log('Form validation failed');
// Prevent resubmission // 현재 상태 출력
if (isSubmitting) { const scenarioValidation = validateFormByScenario();
console.log('Form is already being submitted...'); const commonValidation = validateCommonFields();
return;
}
isSubmitting = true; const isPasswordValid = document.getElementById('isPasswordValid')?.value.trim() === 'true';
btn_register.disabled = true; const isPasswordMatch = document.getElementById('isPasswordMatch')?.value.trim() === 'true';
try {
form.classList.add('was-validated');
if (agreementForm) {
agreementForm.classList.add('was-validated');
}
console.log('Validating form...'); console.log('Validation details:', {
scenarioValidation,
commonValidation,
registrationScenario,
isAuthVerified,
isPasswordValid,
isPasswordMatch
});
if (validateForm()) { let errorMessage = '모든 필수 항목을 입력하고 약관에 동의해주세요.'
console.log('Form validation passed, collecting form data...'); customPopups.showAlert(errorMessage);
const form = collectFormData();
document.body.appendChild(form);
form.submit();
} else {
console.log('Form validation failed');
// 현재 상태 출력
const scenarioValidation = validateFormByScenario();
const commonValidation = validateCommonFields();
const isPasswordValid = document.getElementById('isPasswordValid').value.trim() === 'true';
const isPasswordMatch = document.getElementById('isPasswordMatch').value.trim() === 'true';
console.log('Validation details:', {
scenarioValidation,
commonValidation,
registrationScenario,
isAuthVerified,
isPasswordValid,
isPasswordMatch
});
let errorMessage = '모든 필수 항목을 입력하고 약관에 동의해주세요.'
if (!scenarioValidation) {
customPopups.showAlert(errorMessage);
}
if (!commonValidation) {
customPopups.showAlert(errorMessage);
}
customPopups.showAlert(errorMessage);
// Reset submission state since validation failed
isSubmitting = false;
btn_register.disabled = false;
}
} catch (error) {
console.error('Form submission error:', error);
// Reset submission state on error
isSubmitting = false;
btn_register.disabled = false;
}
// Reset submission state since validation failed
isSubmitting = false;
btn_register.disabled = false;
document.getElementById('loadingOverlay').classList.remove('active');
}
} catch (error) {
console.error('Form submission error:', error);
// Reset submission state on error
isSubmitting = false;
btn_register.disabled = false;
document.getElementById('loadingOverlay').classList.remove('active');
}
}); });
// 신규 이메일 중복 체크 응답 처리 함수 // 신규 이메일 중복 체크 응답 처리 함수
function handleNewEmailValidationResponse(response) { function handleNewEmailValidationResponse(response) {
if (response.valid) { if (response.valid) {
customPopups.showAlert(response.message || '신규 이메일 정보입니다.'); customPopups.showAlert(response.message || '신규 이메일 정보입니다.');
} else { } else {
customPopups.showAlert(response.message || '이미 사용 중인 이메일입니다.'); customPopups.showAlert(response.message || '이미 사용 중인 이메일입니다.');
} }
}
// HTMX 이벤트 리스너 - 모든 HTMX 응답을 중앙에서 처리
document.body.addEventListener('htmx:afterRequest', function (event) {
const targetId = event.detail.target.id;
let response;
try {
response = JSON.parse(event.detail.xhr.response);
} catch (e) {
console.error('응답 JSON 파싱 실패:', e);
customPopups.showAlert('서버 응답을 처리할 수 없습니다.');
return;
}
// 비밀번호 관련 검증은 컴포넌트의 로직을 따르도록 return
if (targetId === 'password-validation' || targetId === 'password-match-validation') {
return;
}
// 나머지 요청들에 대해서만 처리
if (event.detail.xhr.getResponseHeader('X-Custom-Handler') === 'registration-validation') {
if (response && response.message) {
customPopups.showAlert(response.message);
}
return;
}
// 기존 응답 처리 로직
if (targetId === 'email-validation') {
handleEmailValidationResponse(response);
} else if (targetId === 'new-email-validation') {
handleNewEmailValidationResponse(response);
}
// 사업자등록번호와 법인등록번호에 대한 커스텀 이벤트 디스패치
if (targetId === 'compRegNo' || targetId === 'corpRegNo') {
const customEvent = new CustomEvent('htmxResponse', {
detail: {
targetId: targetId,
response: response
} }
});
document.dispatchEvent(customEvent);
}
});
// HTMX 이벤트 리스너 - 모든 HTMX 응답을 중앙에서 처리 // 커스텀 이벤트 리스너
document.body.addEventListener('htmx:afterRequest', function (event) { document.addEventListener('htmxResponse', function (event) {
const targetId = event.detail.target.id; const {targetId, response} = event.detail;
let response;
try { if (targetId === 'compRegNo') {
response = JSON.parse(event.detail.xhr.response); customPopups.showAlert(response.message || (response.valid ? '유효한 사업자등록번호입니다.' : '유효하지 않은 사업자등록번호입니다.'));
} catch (e) { } else if (targetId === 'corpRegNo') {
console.error('응답 JSON 파싱 실패:', e); customPopups.showAlert(response.message || (response.valid ? '유효한 법인등록번호입니다.' : '유효하지 않은 법인등록번호입니다.'));
customPopups.showAlert('서버 응답을 처리할 수 없습니다.'); }
return; });
}
// 비밀번호 관련 검증은 컴포넌트의 로직을 따르도록 return // 인증 완료 이벤트 리스너
if (targetId === 'password-validation' || targetId === 'password-match-validation') { document.addEventListener('authVerified', function () {
return; isAuthVerified = true;
} });
// 나머지 요청들에 대해서만 처리 // 취소 버튼 클릭 시
if (event.detail.xhr.getResponseHeader('X-Custom-Handler') === 'registration-validation') { document.getElementById('cancelButton').addEventListener('click', function () {
if (response && response.message) { location.href = '/login';
customPopups.showAlert(response.message); });
} });
return; </script>
}
// 기존 응답 처리 로직
if (targetId === 'email-validation') {
handleEmailValidationResponse(response);
} else if (targetId === 'new-email-validation') {
handleNewEmailValidationResponse(response);
}
// 사업자등록번호와 법인등록번호에 대한 커스텀 이벤트 디스패치
if (targetId === 'compRegNo' || targetId === 'corpRegNo') {
const customEvent = new CustomEvent('htmxResponse', {
detail: {
targetId: targetId,
response: response
}
});
document.dispatchEvent(customEvent);
}
});
// 커스텀 이벤트 리스너
document.addEventListener('htmxResponse', function (event) {
const {targetId, response} = event.detail;
if (targetId === 'compRegNo') {
customPopups.showAlert(response.message || (response.valid ? '유효한 사업자등록번호입니다.' : '유효하지 않은 사업자등록번호입니다.'));
} else if (targetId === 'corpRegNo') {
customPopups.showAlert(response.message || (response.valid ? '유효한 법인등록번호입니다.' : '유효하지 않은 법인등록번호입니다.'));
}
});
// 인증 완료 이벤트 리스너
document.addEventListener('authVerified', function () {
isAuthVerified = true;
});
// 취소 버튼 클릭 시
document.getElementById('cancelButton').addEventListener('click', function () {
location.href = '/login';
});
});
</script>
</th:block> </th:block>
</body> </body>
</html> </html>
@@ -5,53 +5,70 @@
layout:decorate="~{layout/base_layout}"> layout:decorate="~{layout/base_layout}">
<body> <body>
<th:block th:fragment="agreementContent(termsOfUse, privacyCollect)"> <th:block th:fragment="agreementContent(termsOfUse, privacyCollect)">
<div class="terms_box t_top"> <!-- Agreement Section Header -->
<p>약관 동의</p> <div class="org-section-header org-section-header--agreement">
<h3>약관 동의</h3>
<span class="required-badge">필수 동의</span>
</div> </div>
<form id="agreementForm">
<div class="total_wrap"> <form id="agreementForm" class="agreement-form">
<span class="inp_check b_tit"> <!-- Agree All Checkbox -->
<input type='checkbox' value='selectall' id="agree_all"/> <div class="agreement-all-section">
<label for="agree_all">전체동의</label> <label class="agreement-checkbox-label">
</span> <input type="checkbox" id="agree_all" class="agreement-checkbox-input">
<div class="total_type"> <span class="agreement-checkbox-custom"></span>
<div class="suv_ck"> <span class="agreement-checkbox-text agreement-all-text">전체동의</span>
<ul> </label>
<li> </div>
<span class="inp_check s_tit">
<input type='checkbox' name='termsOfUse' id="termsOfUse" required/> <!-- Divider -->
<label for="termsOfUse"> <div class="agreement-divider"></div>
<span>[필수]</span>「API Portal」 서비스 이용약관
</label> <!-- Individual Agreements -->
</span> <div class="agreement-items-list">
<a class="toggle-btn" data-target="termsOfUseContent"></a> <!-- Terms of Use -->
<div id="termsOfUseContent" class="txt_wrap scrollbar" style="display:none;"> <div class="agreement-item-row">
<div th:utext="${termsOfUse?.contents ?: '이용약관 내용을 불러올 수 없습니다.'}"></div> <label class="agreement-checkbox-label">
</div> <input type="checkbox" name="termsOfUse" id="termsOfUse" class="agreement-checkbox-input" required>
</li> <span class="agreement-checkbox-custom"></span>
</ul> <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="termsOfUseContent">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<div id="termsOfUseContent" class="agreement-content" style="display:none;">
<div class="agreement-scroll">
<div th:utext="${termsOfUse?.contents ?: '이용약관 내용을 불러올 수 없습니다.'}"></div>
</div> </div>
<!-- 개인정보수집동의서 추가 --> </div>
<div class="suv_ck">
<ul> <!-- Privacy Policy -->
<li> <div class="agreement-item-row">
<span class="inp_check s_tit"> <label class="agreement-checkbox-label">
<input type='checkbox' name='privacyCollect' id="privacyCollect" required/> <input type="checkbox" name="privacyCollect" id="privacyCollect" class="agreement-checkbox-input" required>
<th:block th:switch="${registrationType}"> <span class="agreement-checkbox-custom"></span>
<label for="privacyCollect" th:case="'personal'"> <th:block th:switch="${registrationType}">
<span>[필수]</span> Kbank API Portal 회원가입을 위한 개인정보 수집•이용 동의서 (개인회원용) <span class="agreement-checkbox-text" th:case="'personal'">
</label> <span class="agreement-required">[필수]</span>
<label for="privacyCollect" th:case="'corporate'"> <span class="agreement-title">개인정보 수집 및 이용 동의</span>
<span>[필수]</span> Kbank API Portal 회원가입을 위한 개인정보 수집•이용 동의서 (법인회원용) </span>
</label> <span class="agreement-checkbox-text" th:case="'corporate'">
</th:block> <span class="agreement-required">[필수]</span>
</span> <span class="agreement-title">개인정보 수집 및 이용 동의</span>
<a class="toggle-btn" data-target="privacyCollectContent"></a> </span>
<div id="privacyCollectContent" class="txt_wrap scrollbar" style="display:none;"> </th:block>
<div th:utext="${privacyCollect?.contents ?: '개인정보수집동의서 내용을 불러올 수 없습니다.'}"></div> </label>
</div> <button type="button" class="agreement-toggle-icon" data-target="privacyCollectContent">
</li> <i class="fas fa-chevron-down"></i>
</ul> </button>
</div>
<div id="privacyCollectContent" class="agreement-content" style="display:none;">
<div class="agreement-scroll">
<div th:utext="${privacyCollect?.contents ?: '개인정보수집동의서 내용을 불러올 수 없습니다.'}"></div>
</div> </div>
</div> </div>
</div> </div>
@@ -61,49 +78,56 @@
<script th:fragment="agreementScript"> <script th:fragment="agreementScript">
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const agreeAll = document.getElementById('agree_all'); const agreeAll = document.getElementById('agree_all');
const agreementChecks = document.querySelectorAll('.inp_check.s_tit input[type="checkbox"]'); const agreementChecks = document.querySelectorAll('.agreement-checkbox-input[required]');
const toggleBtns = document.querySelectorAll('.toggle-btn'); const toggleIcons = document.querySelectorAll('.agreement-toggle-icon');
// 전체 동의 체크박스 이벤트 // 전체 동의 체크박스 이벤트
agreeAll.addEventListener('change', function () { if (agreeAll) {
agreementChecks.forEach(check => { agreeAll.addEventListener('change', function () {
check.checked = this.checked; agreementChecks.forEach(check => {
check.checked = this.checked;
});
}); });
}); }
// 개별 체크박스 이벤트 // 개별 체크박스 이벤트
agreementChecks.forEach(check => { agreementChecks.forEach(check => {
check.addEventListener('change', function () { check.addEventListener('change', function () {
agreeAll.checked = Array.from(agreementChecks).every(c => c.checked); if (agreeAll) {
agreeAll.checked = Array.from(agreementChecks).every(c => c.checked);
}
}); });
}); });
// 토글 버튼 이벤트 // 토글 아이콘 이벤트
toggleBtns.forEach(btn => { toggleIcons.forEach(icon => {
btn.addEventListener('click', function (e) { icon.addEventListener('click', function (e) {
e.preventDefault(); e.preventDefault();
const targetId = this.getAttribute('data-target'); const targetId = this.getAttribute('data-target');
const targetContent = document.getElementById(targetId); const targetContent = document.getElementById(targetId);
const parentSection = this.closest('.suv_ck'); const parentRow = this.closest('.agreement-item-row');
// 현재 섹션이 이미 열려있는지 확인 // 현재 섹션이 이미 열려있는지 확인
const isCurrentlyOpen = targetContent.style.display === 'block'; const isCurrentlyOpen = targetContent.style.display === 'block';
document.querySelectorAll('.txt_wrap').forEach(content => { // 모든 섹션 닫기
document.querySelectorAll('.agreement-content').forEach(content => {
content.style.display = 'none'; content.style.display = 'none';
}); });
document.querySelectorAll('.toggle-btn').forEach(button => { document.querySelectorAll('.agreement-toggle-icon').forEach(button => {
button.classList.remove('active'); button.classList.remove('active');
}); });
document.querySelectorAll('.suv_ck').forEach(section => { document.querySelectorAll('.agreement-item-row').forEach(item => {
section.classList.remove('active'); item.classList.remove('active');
}); });
// 현재 섹션이 닫혀있었다면 열기 // 현재 섹션이 닫혀있었다면 열기
if (!isCurrentlyOpen) { if (!isCurrentlyOpen) {
targetContent.style.display = 'block'; targetContent.style.display = 'block';
this.classList.add('active'); this.classList.add('active');
parentSection.classList.add('active'); if (parentRow) {
parentRow.classList.add('active');
}
setTimeout(() => { setTimeout(() => {
targetContent.scrollIntoView({ targetContent.scrollIntoView({
@@ -1,65 +1,76 @@
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" <html lang="ko" xmlns:th="http://www.thymeleaf.org"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/kbank_base_layout}"> layout:decorate="~{layout/kjbank_signup_layout}">
<body> <body>
<section layout:fragment="contentFragment" class="content"> <section layout:fragment="title">
<script th:src="@{/js/htmx.min.js}"></script> <div class="org-page-title-banner">
<div class="content_wrap"> <img th:src="@{/img/user_register_title.png}" alt="개인회원가입" class="title-image">
<div class="sub_title2"> <h1 th:text="${!isInvited ? '개인회원가입' : '법인회원가입'}">개인회원가입</h1>
<h2 class="title add" th:if="${!isInvited}">개인회원가입</h2> </div>
<h2 class="title add" th:if="${isInvited}">법인회원가입</h2> </section>
</div>
<div class="inner i_cs h_inner2">
<div class="top_info" th:if="${!isInvited}">
<ul>
<li>서비스 또는 API 사용을 원하실 경우 법인회원 승인 후 이용하실 수 있습니다.</li>
</ul>
</div>
<!-- 약관동의 섹션 -->
<input type="hidden" id="csrf" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<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">
<!-- 법인 정보 섹션, 초대받은 경우에만 -->
<div class="terms_box" th:if="${isInvited}">
<p>법인 정보</p>
</div>
<div class="form_type" th:if="${isInvited}"> <section layout:fragment="contentFragment">
<div class="info1"> <div class="org-register-page">
<p class="title w_tit"> <div class="org-register-container">
<span>법인명</span> <!-- Hidden Fields -->
</p> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<div class="info_line"> <input type="hidden" name="registrationType" th:value="${registrationType}"/>
<div class="info_box1 w_inp4">
<input type="text" name="companyName" class="common_input_type_1" <!-- Alert Container -->
th:classappend="${isInvited ? 'input-readonly' : ''}" th:value="${orgName}" readonly> <div id="alertContainer"></div>
</div>
</div> <!-- Info Notice -->
</div> <div class="org-info-notice" th:if="${!isInvited}">
</div> <ul>
<!-- end 법인 정보 섹션, 초대받은 경우에만 --> <li>서비스 또는 API 사용을 원하실 경우 법인회원 승인 후 이용하실 수 있습니다.</li>
<div class="terms_box"> </ul>
<p>기본 정보</p> </div>
<span class="dot">필수 입력란입니다</span>
</div> <!-- Agreement Section -->
<div class="form_type" th:if="${registrationType == isInvited ? 'corporate' :'personal'}"> <th:block th:replace="~{apps/register/userAgreementContent :: agreementContent(${termsOfUse}, ${privacyCollect})}"></th:block>
<th:block th:replace="~{apps/register/components/commonUserInfoForm :: commonUserInfo}"></th:block>
<th:block th:replace="~{apps/register/components/newUserInfoForm :: newUserForm}"></th:block> <form name="portalUser" id="registerForm" role="form" th:action="@{/signup/portalUser}"
</div> th:object="${portalUser}" method="post">
<div class="form_type">
<div class="btn_application"> <!-- Corporate Information Section (invited users only) -->
<a type="button" class="common_btn_type_1 gray" id="cancelButton"><span>취소</span></a> <div th:if="${isInvited}">
<div class="btn_gap"></div> <div class="org-section-header org-section-header--agreement">
<a type="button" class="common_btn_type_1 btn_register"><span>가입신청</span></a> <h3>법인 정보</h3>
</div> </div>
</div>
</form> <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>
<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="org-btn org-btn-secondary" id="cancelButton">취소</button>
<button type="button" class="org-btn org-btn-primary btn_register">가입신청</button>
</div>
</form>
<!-- Loading Overlay -->
<div class="org-loading-overlay" id="loadingOverlay">
<div class="spinner"></div>
</div>
</div> </div>
</div>
</section> </section>
<th:block layout:fragment="contentScript"> <th:block layout:fragment="contentScript">
<script th:if="${error}" th:inline="javascript"> <script th:if="${error}" th:inline="javascript">
@@ -70,128 +81,119 @@
<script th:replace="~{apps/register/userAgreementContent :: agreementScript}"></script> <script th:replace="~{apps/register/userAgreementContent :: agreementScript}"></script>
<script th:replace="~{apps/register/components/commonUserInfoForm :: commonUserInfoScript}"></script> <script th:replace="~{apps/register/components/commonUserInfoForm :: commonUserInfoScript}"></script>
<script th:replace="~{apps/register/components/newUserInfoForm :: newUserScript}"></script> <script th:replace="~{apps/register/components/newUserInfoForm :: newUserScript}"></script>
<script> <script th:inline="javascript">
let form = document.getElementById('registerForm'); document.addEventListener('DOMContentLoaded', function () {
let agreementForm = document.getElementById('agreementForm'); let form = document.getElementById('registerForm');
let btn_register = document.querySelector('.btn_register'); let agreementForm = document.getElementById('agreementForm');
let isAuthVerified = false; let btn_register = document.querySelector('.btn_register');
let isAuthVerified = false;
// 인증 완료 이벤트 리스너 // 인증 완료 이벤트 리스너
document.addEventListener('authVerified', function() { document.addEventListener('authVerified', function() {
isAuthVerified = true; isAuthVerified = true;
}); });
// 이메일 유효성 검사 함수 // 이메일 유효성 검사 함수
function isValidEmail(email) { function isValidEmail(email) {
const safeEmailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return safeEmailRegex.test(email); return emailRegex.test(email);
}
// 폼 유효성 검사 함수
function checkFormValidity() {
return isValidEmail($('#loginId').val()) &&
($('#password').val() === $('#password2').val()) &&
($('#userName').val().trim() !== '') &&
$('#termsOfUse').prop('checked') &&
$('#privacyCollect').prop('checked') &&
(combineMobileNumber('#mobileNumber') !== null) &&
isAuthVerified; // 인증 여부 확인
}
let isSubmitting = false;
// 등록 버튼 클릭 이벤트
btn_register.addEventListener('click', function(event) {
event.preventDefault();
event.stopPropagation();
// Prevent resubmission
if (isSubmitting) {
console.log('Form is already being submitted...');
return;
} }
isSubmitting = true; // 폼 유효성 검사 함수
btn_register.disabled = true; function checkFormValidity() {
const loginIdElement = $('#loginId');
const userNameElement = $('#userName');
const termsOfUseElement = $('#termsOfUse');
const privacyCollectElement = $('#privacyCollect');
const mobileNumberElement = $('#mobileNumber');
const isPasswordValid = $('#isPasswordValid').val() === 'true';
const isPasswordMatch = $('#isPasswordMatch').val() === 'true';
form.classList.add('was-validated'); return isValidEmail(loginIdElement.val()) &&
agreementForm.classList.add('was-validated'); isPasswordValid &&
isPasswordMatch &&
userNameElement.val().trim() !== '' &&
termsOfUseElement.prop('checked') &&
privacyCollectElement.prop('checked') &&
mobileNumberElement.val().trim() !== '' &&
isAuthVerified;
}
if (checkFormValidity() && form.checkValidity() && agreementForm.checkValidity()) { let isSubmitting = false;
// 약관 동의 폼의 데이터를 hidden input으로 추가
let agreementFormData = new FormData(agreementForm); // 등록 버튼 클릭 이벤트
for (let pair of agreementFormData.entries()) { btn_register.addEventListener('click', function(event) {
if (!form.querySelector('input[name="' + pair[0] + '"]')) { event.preventDefault();
let input = document.createElement('input'); event.stopPropagation();
input.type = 'hidden';
input.name = pair[0]; // Prevent resubmission
input.value = pair[1]; if (isSubmitting) {
form.appendChild(input); console.log('Form is already being submitted...');
} return;
} }
// 불필요한 hidden input 제거 isSubmitting = true;
const unnecessaryFields = ['isPasswordValid', 'isPasswordMatch']; btn_register.disabled = true;
unnecessaryFields.forEach(fieldName => {
const field = form.querySelector(`input[name="${fieldName}"]`); // Show loading overlay
if (field) { document.getElementById('loadingOverlay').classList.add('active');
field.remove();
try {
form.classList.add('was-validated');
if (agreementForm) {
agreementForm.classList.add('was-validated');
} }
});
// authCompletedYn 값 설정 if (checkFormValidity() && form.checkValidity() && (agreementForm ? agreementForm.checkValidity() : true)) {
const authCompletedField = document.createElement('input'); // 약관 동의 폼의 데이터를 hidden input으로 추가
authCompletedField.type = 'hidden'; if (agreementForm) {
authCompletedField.name = 'authCompletedYn'; let agreementFormData = new FormData(agreementForm);
authCompletedField.value = isAuthVerified ? 'Y' : 'N'; for (let pair of agreementFormData.entries()) {
form.appendChild(authCompletedField); if (!form.querySelector('input[name="' + pair[0] + '"]')) {
let input = document.createElement('input');
input.type = 'hidden';
input.name = pair[0];
input.value = pair[1];
form.appendChild(input);
}
}
}
form.submit(); // 불필요한 hidden input 제거
} else { const unnecessaryFields = ['isPasswordValid', 'isPasswordMatch'];
customPopups.showAlert('모든 필수 항목을 입력하고 약관에 동의해주세요.'); unnecessaryFields.forEach(fieldName => {
isSubmitting = false; const field = form.querySelector(`input[name="${fieldName}"]`);
btn_register.disabled = false; if (field) {
} field.remove();
}); }
});
// 취소버튼 클릭시 // authCompletedYn 값 설정
document.getElementById('cancelButton').addEventListener('click', function() { const authCompletedField = document.createElement('input');
location.href = '/login'; authCompletedField.type = 'hidden';
}); authCompletedField.name = 'authCompletedYn';
authCompletedField.value = isAuthVerified ? 'Y' : 'N';
form.appendChild(authCompletedField);
// HTMX 이벤트 리스너 - 모든 HTMX 응답을 중앙에서 처리 form.submit();
document.body.addEventListener('htmx:afterRequest', function(event) {
const targetId = event.detail.target.id;
let response;
try {
response = JSON.parse(event.detail.xhr.response);
} catch (e) {
console.error('응답 JSON 파싱 실패:', e);
customPopups.showAlert('서버 응답을 처리할 수 없습니다.');
return;
}
// 커스텀 헤더를 체크하여 등록 검증 요청 처리
if (event.detail.xhr.getResponseHeader('X-Custom-Handler') === 'registration-validation') {
if (response && response.message) {
if (response.status === 'SUCCESS') {
customPopups.showSuccess(response.message);
} else { } else {
customPopups.showAlert(response.message); customPopups.showAlert('모든 필수 항목을 입력하고 약관에 동의해주세요.');
isSubmitting = false;
btn_register.disabled = false;
document.getElementById('loadingOverlay').classList.remove('active');
} }
} catch (error) {
console.error('Form submission error:', error);
isSubmitting = false;
btn_register.disabled = false;
document.getElementById('loadingOverlay').classList.remove('active');
} }
return; });
}
// 응답 처리 로직 // 취소 버튼 클릭 시
if (targetId === 'email-validation') { document.getElementById('cancelButton').addEventListener('click', function() {
// commonUserInfoForm의 함수 호출 location.href = '/login';
handleEmailValidationResponse(response); });
} else if (targetId === 'password-validation' || targetId === 'password-match-validation') {
// 비밀번호 검증은 컴포넌트에서 처리하도록 함
return;
}
}); });
</script> </script>
</th:block> </th:block>
@@ -0,0 +1,30 @@
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head th:replace="fragment/kjbank/head :: headFragment">
<meta charset="utf-8">
</head>
<body>
<th:block th:replace="fragment/kjbank/header_container :: headerFragment"></th:block>
<th:block th:replace="fragment/kjbank/header_container :: headerScript"></th:block>
<div class="loading-overlay" style="display: none;">
<div class="loading-spinner"></div>
</div>
<th:block layout:fragment="title">
</th:block>
<div class="container">
<th:block layout:fragment="contentFragment">
</th:block>
</div>
<th:block layout:fragment="contentScript">
</th:block>
<footer th:replace="fragment/kjbank/footer :: footerFragment"></footer>
<section th:replace="fragment/popup/customPopups :: customPopups"></section>
<section layout:fragment="pagePopups"></section>
<script th:src="@{/js/popup/custom-popups.js}"></script>
</body>
</html>