회원가입 디자인 적용

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 {
max-width: 1400px;
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-purple: #A78BFA; // Purple accent
$main-bg: #EDF9FE;
// Neutral colors
$text-dark: #1A1A2E; // Main text color
$text-gray: #64748B; // Secondary text
@@ -25,6 +27,7 @@ $black: #000000; // Pure black
$gray-800: #1F2937; // Dark gray
$gray-900: #111827; // Darker gray
// Gradients
$gradient-primary: linear-gradient(135deg, $primary-blue 0%, $secondary-blue 100%);
$gradient-accent: linear-gradient(135deg, $accent-cyan 0%, $primary-blue 100%);
@@ -118,4 +121,4 @@ $button-padding-y: 16px;
$card-padding: 32px;
$input-padding-x: 16px;
$input-padding-y: 12px;
$input-height: 48px;
$input-height: 48px;
+1
View File
@@ -48,6 +48,7 @@
@import 'pages/apikey-detail';
@import 'pages/notice';
@import 'pages/inquiry';
@import 'pages/org-register';
@import 'pages/user-management';
// 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>
<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:hx-on="http://www.w3.org/1999/xhtml">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_base_layout}">
<body>
<th:block th:fragment="commonUserInfo">
<div class="info1 pt28">
<p class="title">
<span class="dot">이메일 아이디</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="text" id="userId" class="common_input_type_1 w_inp"
<div class="org-form-group">
<label class="org-form-label">
<span class="required">*</span>
이메일 아이디
</label>
<div class="org-form-input-wrapper">
<div class="org-compound-input">
<input type="text" id="userId" class="org-form-input"
th:classappend="${isInvited ? 'input-readonly' : ''}"
maxlength="30"
th:value="${emailId}"
th:readonly="${isInvited}"
th:placeholder="#{portalUser.Register.email}">
<span class="special_t">@</span>
<input type="text" id="domain" class="common_input_type_1 w_inp2"
<span class="separator">@</span>
<input type="text" id="domain" class="org-form-input"
th:classappend="${isInvited ? 'input-readonly' : ''}"
maxlength="50"
th:value="${domain}"
th:readonly="${isInvited}"
th:placeholder="#{portalUser.Register.domain}">
<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>
<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 id="email-validation"></div>
</div>
</th:block>
</body>
<th:block>
<script th:fragment="commonUserInfoScript">
<script th:fragment="commonUserInfoScript" th:inline="javascript">
// 이메일 조합 함수
function combineEmail() {
let emailId = document.getElementById('userId').value;
let domain = document.getElementById('domain').value;
let emailId = $('#userId').val();
let domain = $('#domain').val();
let combined = emailId + '@' + domain;
let loginIdInput = document.getElementById('loginId');
if (loginIdInput) {
loginIdInput.value = combined;
}
$('#loginId').val(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');
const individualConversionForm = $('#individualConversionForm');
const emailChangeForm = $('#emailChangeForm');
const newUserForm = $('#newUserForm, #newUserFields');
const userIdInput = $('#userId');
const domainInput = $('#domain');
const checkButton = $('.btn_check_email');
const loginIdInput = $('#loginId');
const scenarioInput = $('#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);
}
}
scenarioInput.val('new');
customPopups.showAlert(response.message);
if (checkButton) {
checkButton.style.display = 'none';
}
checkButton.hide();
if (userIdInput && domainInput && loginIdInput) {
userIdInput.disabled = true;
domainInput.disabled = true;
userIdInput.style.backgroundColor = '#e0e0e0';
domainInput.style.backgroundColor = '#e0e0e0';
userIdInput.prop('disabled', true);
domainInput.prop('disabled', true);
userIdInput.css('backgroundColor', '#e0e0e0');
domainInput.css('backgroundColor', '#e0e0e0');
const combinedEmail = `${userIdInput.value.trim()}@${domainInput.value.trim()}`;
loginIdInput.value = combinedEmail;
}
const combinedEmail = `${userIdInput.val().trim()}@${domainInput.val().trim()}`;
loginIdInput.val(combinedEmail);
if (newUserForm) newUserForm.style.display = 'block';
if (individualConversionForm) individualConversionForm.style.display = 'none';
if (emailChangeForm) emailChangeForm.style.display = 'none';
newUserForm.show();
individualConversionForm.hide();
emailChangeForm.hide();
break;
case "conversionOrChange":
@@ -104,51 +85,33 @@
response.message,
function () {
// "이메일 유지" 선택 시 동작
if (scenarioInput) {
scenarioInput.value = 'retain';
if (!scenarioInput.form) {
document.getElementById('registerForm').appendChild(scenarioInput);
}
}
scenarioInput.val('retain');
if (individualConversionForm) individualConversionForm.style.display = 'block';
if (emailChangeForm) emailChangeForm.style.display = 'none';
if (newUserForm) newUserForm.style.display = 'none';
individualConversionForm.show();
emailChangeForm.hide();
newUserForm.hide();
if (userIdInput && domainInput) {
userIdInput.disabled = true;
domainInput.disabled = true;
userIdInput.style.backgroundColor = '#e0e0e0';
domainInput.style.backgroundColor = '#e0e0e0';
}
userIdInput.prop('disabled', true);
domainInput.prop('disabled', true);
userIdInput.css('backgroundColor', '#e0e0e0');
domainInput.css('backgroundColor', '#e0e0e0');
if (checkButton) {
checkButton.style.display = 'none';
}
checkButton.hide();
},
function () {
// "이메일 전환" 선택 시 동작
if (scenarioInput) {
scenarioInput.value = 'change';
if (!scenarioInput.form) {
document.getElementById('registerForm').appendChild(scenarioInput);
}
}
scenarioInput.val('change');
if (emailChangeForm) emailChangeForm.style.display = 'block';
if (individualConversionForm) individualConversionForm.style.display = 'none';
if (newUserForm) newUserForm.style.display = 'none';
emailChangeForm.show();
individualConversionForm.hide();
newUserForm.hide();
if (userIdInput && domainInput) {
userIdInput.disabled = true;
domainInput.disabled = true;
userIdInput.style.backgroundColor = '#e0e0e0';
domainInput.style.backgroundColor = '#e0e0e0';
}
userIdInput.prop('disabled', true);
domainInput.prop('disabled', true);
userIdInput.css('backgroundColor', '#e0e0e0');
domainInput.css('backgroundColor', '#e0e0e0');
if (checkButton) {
checkButton.style.display = 'none';
}
checkButton.hide();
}
);
break;
@@ -169,11 +132,35 @@
}
// 이메일 입력 필드에 이벤트 리스너 추가
document.addEventListener('DOMContentLoaded', function () {
let userIdInput = document.getElementById("userId");
let domainInput = document.getElementById("domain");
if (userIdInput) userIdInput.addEventListener("input", combineEmail);
if (domainInput) domainInput.addEventListener("input", combineEmail);
$(document).ready(function () {
$('#userId, #domain').on('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>
@@ -5,127 +5,94 @@
<th:block th:fragment="newUserForm">
<div id="newUserFields">
<!-- 성명 -->
<div class="info1 pt28">
<p class="title">
<span class="dot">성명</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="text" th:field="*{userName}" id="userName" class="common_input_type_1"
th:placeholder="#{portalUser.Register.userName}">
</div>
<div class="btn_check pc-only"></div>
<div class="org-form-group">
<label class="org-form-label">
<span class="required">*</span>
성명
</label>
<div class="org-form-input-wrapper">
<input type="text" th:field="*{userName}" id="userName" class="org-form-input"
th:placeholder="#{portalUser.Register.userName}">
</div>
</div>
<!-- 휴대폰 번호 입력 -->
<div class="info1 pt28">
<p class="title">
<span class="dot">휴대폰 번호</span>
</p>
<div class="info_line">
<div class="info_box1">
<span class="input">
<div class="custom_select ct_sc h_inp2" id="phoneMobile">
<div class="select_selected">선택</div>
<ul class="common_selecttype select_items select_hide ct_sc2">
<li>010</li>
<li>011</li>
<li>017</li>
<li>016</li>
<li>018</li>
</ul>
</div>
</span>
<span class="dash">-</span>
<span class="input">
<input type="text" id="cellPhone1" maxlength="4" class="common_input_type_1 w_inp3"
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 class="org-form-group">
<label class="org-form-label">
<span class="required">*</span>
휴대폰 번호
</label>
<div class="org-form-input-wrapper">
<div class="org-compound-input">
<select class="org-form-select phone-number" id="phoneMobile">
<option value="">선택</option>
<option value="010">010</option>
<option value="011">011</option>
<option value="017">017</option>
<option value="016">016</option>
<option value="018">018</option>
</select>
<span class="separator">-</span>
<input type="text" id="cellPhone1" maxlength="4" class="org-form-input phone-number"
th:placeholder="#{portal.Register.phone1}">
<span class="separator">-</span>
<input type="text" id="cellPhone2" maxlength="4" class="org-form-input phone-number"
th:placeholder="#{portal.Register.phone2}">
</div>
<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 id="mobile-validation"></div>
<!-- 인증번호 입력 -->
<div class="info1 pt28" id="authNumberContainer">
<p class="title">
<span class="dot">인증번호 입력</span>
</p>
<div class="info_line">
<div class="info_box1">
<div class="certify_box">
<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>
<span class="certify_time" 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 class="org-form-group" id="authNumberContainer" style="display: none;">
<label class="org-form-label">
<span class="required">*</span>
인증번호 입력
</label>
<div class="org-form-input-wrapper">
<div class="org-auth-input-group">
<input type="text" th:field="*{authNumber}" id="authNumber" maxlength="6" pattern="[0-9]*"
inputmode="numeric" class="org-form-input" th:placeholder="#{ncrdRegister.validate.authNumber}" required>
<span class="org-timer" id="certify_time">03:00</span>
</div>
<button type="button" class="org-btn org-btn-check btn_verify_auth">
인증번호 확인
</button>
<div id="authNumber-validation" class="org-validation-message"></div>
</div>
</div>
<div id="authNumber-validation"></div>
<div class="info1 pt28">
<p class="title">
<span class="dot">비밀번호</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="password" name="password" id="password" class="common_input_type_1 w_input"
th:placeholder="#{portalUser.Register.pass}"
th:hx-post="@{/check_password}"
hx-trigger="blur"
hx-include="#mobileNumber, #loginId"
hx-target="#password-validation"
hx-swap="innerHTML">
<input type="hidden" name="isPasswordValid" id="isPasswordValid"/>
</div>
<div class="btn_check pc-only"></div>
<!-- 비밀번호 -->
<div class="org-form-group">
<label class="org-form-label">
<span class="required">*</span>
비밀번호
</label>
<div class="org-form-input-wrapper">
<input type="password" name="password" id="password" class="org-form-input"
th:placeholder="#{portalUser.Register.pass}">
<input type="hidden" name="isPasswordValid" id="isPasswordValid"/>
<div id="password-validation" class="org-validation-message"></div>
</div>
</div>
<div id="password-validation" class="validation-message"></div>
<!-- 비밀번호 확인 -->
<div class="info1 pt28">
<p class="title">
<span class="dot">비밀번호 확인</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="password" name="password2" id="password2" class="common_input_type_1 w_input"
th:placeholder="#{portalUser.Register.passConfirm}"
th:hx-post="@{/check_password_match}"
hx-trigger="blur"
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 class="org-form-group">
<label class="org-form-label">
<span class="required">*</span>
비밀번호 확인
</label>
<div class="org-form-input-wrapper">
<input type="password" name="password2" id="password2" class="org-form-input"
th:placeholder="#{portalUser.Register.passConfirm}">
<input type="hidden" name="isPasswordMatch" id="isPasswordMatch"/>
<div id="password-match-validation" class="org-validation-message"></div>
</div>
</div>
<div id="password-match-validation" class="validation-message"></div>
</div>
</th:block>
@@ -134,16 +101,16 @@
<script th:fragment="newUserScript">
// 휴대폰 번호 조합 함수
function combineMobileNumber() {
let mobileElement = document.querySelector('#phoneMobile .select_selected');
if (!mobileElement) {
console.error('Mobile element not found');
let mobileSelect = document.getElementById('phoneMobile');
if (!mobileSelect) {
console.error('Mobile select element not found');
return null;
}
let prefix = mobileElement.textContent.trim();
let prefix = mobileSelect.value;
let middle = document.getElementById('cellPhone1').value.trim();
let last = document.getElementById('cellPhone2').value.trim();
if (prefix === '선택' || !middle || !last) {
if (!prefix || !middle || !last) {
return null;
}
@@ -159,7 +126,6 @@
}
document.addEventListener('DOMContentLoaded', function () {
activateSelect();
let countdownInterval;
let isAuthVerified = false;
@@ -182,11 +148,9 @@
combineMobileNumber();
});
// select_selected 변경 감지
$('#phoneMobile .select_items li').on('click', function () {
setTimeout(() => {
combineMobileNumber();
}, 100);
// select 변경 감지
$('#phoneMobile').on('change', function () {
combineMobileNumber();
});
// 인증번호 입력 제한
@@ -215,8 +179,10 @@
function handleAuthNumberRequest(response) {
if (response.valid) {
let authButton = document.querySelector(".btn_auth");
let authContainer = document.getElementById("authNumberContainer");
if (authButton) authButton.disabled = true;
if (authContainer) authContainer.style.display = 'flex';
startAuthNumberTimer();
customPopups.showAlert(response.message || "인증번호가 발송되었습니다.");
} 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));
let authNumberInput = document.getElementById("authNumber");
@@ -275,76 +242,107 @@
}
// 인증번호 요청 버튼 이벤트 리스너
let authButton = document.querySelector(".btn_auth");
if (authButton) {
authButton.addEventListener("click", function (event) {
let mobileNumber = combineMobileNumber();
if (!mobileNumber) {
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;
}
$('.btn_auth').on('click', function () {
let mobileNumber = combineMobileNumber();
if (!mobileNumber) {
customPopups.showAlert("휴대폰 번호를 올바르게 입력해주세요.");
return;
}
$('#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>
@@ -1,287 +1,274 @@
<!doctype html>
<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>
<th:block th:fragment="orgInfoForm">
<div class="form_type">
<div class="info1">
<p class="title">
<span class="dot">사업자등록번호</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="text" class="common_input_type_1 w_inp5 business-number" id="compRegNo1" maxlength="3"
th:placeholder="#{portalOrg.Register.compRegNo1}" required>
<span class="dash">-</span>
<input type="text" class="common_input_type_1 w_inp6 business-number" id="compRegNo2" maxlength="2"
th:placeholder="#{portalOrg.Register.compRegNo2}" required>
<span class="dash">-</span>
<input type="text" class="common_input_type_1 w_inp7 business-number" id="compRegNo3" maxlength="5"
th:placeholder="#{portalOrg.Register.compRegNo3}" required>
</div>
<div class="btn_check pc-only"></div>
<input type="hidden" id="compRegNo" th:field="*{compRegNo}" hx-trigger="change delay:1s"/>
<!-- Business Registration Number -->
<div class="org-form-group">
<label class="org-form-label">
<span class="required">*</span>
사업자등록번호
</label>
<div class="org-form-input-wrapper">
<div class="org-compound-input">
<input type="text" class="org-form-input business-number" id="compRegNo1" maxlength="3"
placeholder="000" required>
<span class="separator">-</span>
<input type="text" class="org-form-input business-number" id="compRegNo2" maxlength="2"
placeholder="00" required>
<span class="separator">-</span>
<input type="text" class="org-form-input business-number" id="compRegNo3" maxlength="5"
placeholder="00000" required>
</div>
<input type="hidden" id="compRegNo" th:field="*{compRegNo}"/>
<div id="compRegNo-validation" class="org-validation-message"></div>
</div>
</div>
<div class="info1 pt28">
<p class="title">
<span class="dot">법인등록번호</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="text" class="common_input_type_1 w_inp8 corp-reg-no" id="corpRegNo1" maxlength="6"
th:placeholder="#{portalOrg.Register.corpRegNo1}" required>
<span class="dash">-</span>
<input type="text" class="common_input_type_1 w_inp9 corp-reg-no" id="corpRegNo2" maxlength="7"
th:placeholder="#{portalOrg.Register.corpRegNo2}" required>
</div>
<div class="btn_check pc-only"></div>
<input type="hidden" id="corpRegNo" th:field="*{corpRegNo}" hx-trigger="change delay:1s"/>
<!-- Corporate Registration Number -->
<div class="org-form-group">
<label class="org-form-label">
<span class="required">*</span>
법인등록번호
</label>
<div class="org-form-input-wrapper">
<div class="org-compound-input">
<input type="text" class="org-form-input corp-reg-no" id="corpRegNo1" maxlength="6"
placeholder="000000" required>
<span class="separator">-</span>
<input type="text" class="org-form-input corp-reg-no" id="corpRegNo2" maxlength="7"
placeholder="0000000" required>
</div>
<input type="hidden" id="corpRegNo" th:field="*{corpRegNo}"/>
<div id="corpRegNo-validation" class="org-validation-message"></div>
</div>
</div>
<div class="info1 pt28">
<p class="title">
<span class="dot">법인명</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="text" class="common_input_type_1" id="orgName" th:field="*{orgName}"
th:placeholder="#{portalOrg.Register.orgName}"
th:classappend="${#fields.hasErrors('orgName')}? 'is-invalid'" required>
</div>
<div class="btn_check pc-only"></div>
<div th:if="${#fields.hasErrors('orgName')}" class="invalid-feedback">
<span th:errors="*{orgName}"></span>
</div>
<!-- Company Name -->
<div class="org-form-group">
<label class="org-form-label" for="orgName">
<span class="required">*</span>
법인명
</label>
<div class="org-form-input-wrapper">
<input type="text" class="org-form-input" id="orgName" th:field="*{orgName}"
placeholder="법인명을 입력하세요"
th:classappend="${#fields.hasErrors('orgName')}? 'is-invalid'" required>
<div th:if="${#fields.hasErrors('orgName')}" class="org-validation-message error">
<span th:errors="*{orgName}"></span>
</div>
</div>
</div>
<div class="info1 pt28">
<p class="title">
<span class="dot">대표자 성명</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="text" class="common_input_type_1" id="ceoName" th:field="*{ceoName}"
th:placeholder="#{portalOrg.Register.ceoName}"
th:classappend="${#fields.hasErrors('ceoName')}? 'is-invalid'" required>
</div>
<div class="btn_check pc-only"></div>
<div th:if="${#fields.hasErrors('ceoName')}" class="invalid-feedback">
<span th:errors="*{ceoName}"></span>
</div>
<!-- CEO Name -->
<div class="org-form-group">
<label class="org-form-label" for="ceoName">
<span class="required">*</span>
대표자 성명
</label>
<div class="org-form-input-wrapper">
<input type="text" class="org-form-input" id="ceoName" th:field="*{ceoName}"
placeholder="대표자 성명을 입력하세요"
th:classappend="${#fields.hasErrors('ceoName')}? 'is-invalid'" required>
<div th:if="${#fields.hasErrors('ceoName')}" class="org-validation-message error">
<span th:errors="*{ceoName}"></span>
</div>
</div>
</div>
<div class="info1 pt28">
<p class="title">
<span class="dot">사업장 소재지</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="text" class="common_input_type_1" id="orgAddr" th:field="*{orgAddr}"
th:placeholder="#{portalOrg.Register.orgAddr}"
th:classappend="${#fields.hasErrors('orgAddr')}? 'is-invalid'" required>
</div>
<div class="btn_check pc-only"></div>
<div th:if="${#fields.hasErrors('orgAddr')}" class="invalid-feedback">
<span th:errors="*{orgAddr}"></span>
</div>
<!-- Business Address -->
<div class="org-form-group">
<label class="org-form-label" for="orgAddr">
<span class="required">*</span>
사업장 소재지
</label>
<div class="org-form-input-wrapper">
<input type="text" class="org-form-input" id="orgAddr" th:field="*{orgAddr}"
placeholder="사업장 소재지를 입력하세요"
th:classappend="${#fields.hasErrors('orgAddr')}? 'is-invalid'" required>
<div th:if="${#fields.hasErrors('orgAddr')}" class="org-validation-message error">
<span th:errors="*{orgAddr}"></span>
</div>
</div>
</div>
<div class="info1 pt28">
<p class="title">
<span class="dot">업태</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="text" class="common_input_type_1 w_input" id="orgSectors" th:field="*{orgSectors}"
th:placeholder="#{portalOrg.Register.orgSectors}">
</div>
<div class="btn_check pc-only"></div>
</div>
<!-- Business Type -->
<div class="org-form-group">
<label class="org-form-label" for="orgSectors">
<span class="required">*</span>
업태
</label>
<div class="org-form-input-wrapper">
<input type="text" class="org-form-input" id="orgSectors" th:field="*{orgSectors}"
placeholder="업태를 입력하세요" required>
</div>
</div>
<div class="info1 pt28">
<p class="title">
<span class="dot">업종</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="text" class="common_input_type_1" id="orgIndustryType" th:field="*{orgIndustryType}"
th:placeholder="#{portalOrg.Register.orgIndustryType}">
</div>
<div class="btn_check pc-only"></div>
</div>
<!-- Industry Type -->
<div class="org-form-group">
<label class="org-form-label" for="orgIndustryType">
<span class="required">*</span>
업종
</label>
<div class="org-form-input-wrapper">
<input type="text" class="org-form-input" id="orgIndustryType" th:field="*{orgIndustryType}"
placeholder="업종을 입력하세요" required>
</div>
</div>
<div class="info1 pt28">
<p class="title">
<span class="dot">회사 전화번호</span>
</p>
<div class="info_line">
<div class="info_box1">
<span class="input">
<div class="custom_select ct_sc h_inp2" id="orgAreaCode">
<div class="select_selected active">선택</div>
<ul class="common_selecttype select_hide select_items ct_sc2">
<li>국번없음</li>
<li>02</li>
<li>031</li>
<li>032</li>
<li>033</li>
<li>041</li>
<li>042</li>
<li>043</li>
<li>044</li>
<li>051</li>
<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="orgPhoneNumber1" 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="orgPhoneNumber2" class="common_input_type_1 w_inp3" maxlength="4"
th:placeholder="#{portal.Register.phone2}">
</span>
</div>
<input type="hidden" id="orgPhoneNumber" th:field="*{orgPhoneNumber}">
<div class="btn_check pc-only"></div>
<!-- Company Phone Number -->
<div class="org-form-group">
<label class="org-form-label">
<span class="required">*</span>
회사 전화번호
</label>
<div class="org-form-input-wrapper">
<div class="org-compound-input">
<select class="org-form-select" id="orgAreaCode" required>
<option value="">선택</option>
<option value="국번없음">국번없음</option>
<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="orgPhoneNumber1" class="org-form-input" maxlength="4"
placeholder="0000">
<span class="separator">-</span>
<input type="text" id="orgPhoneNumber2" class="org-form-input" maxlength="4"
placeholder="0000">
</div>
<input type="hidden" id="orgPhoneNumber" th:field="*{orgPhoneNumber}">
</div>
</div>
<!-- Service Name -->
<div class="org-form-group">
<label class="org-form-label" for="serviceName">
<span class="required">*</span>
서비스명
</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 class="info1 pt28">
<p class="title">
<span class="dot">서비스명</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="text" class="common_input_type_1" id="serviceName" th:field="*{serviceName}"
th:placeholder="#{portalOrg.Register.serviceName}"
th:classappend="${#fields.hasErrors('serviceName')}? 'is-invalid'" required>
</div>
<div class="btn_check pc-only"></div>
<div th:if="${#fields.hasErrors('serviceName')}" class="invalid-feedback">
<span th:errors="*{serviceName}"></span>
</div>
</div>
<!-- Customer Service Phone Number -->
<div class="org-form-group">
<label class="org-form-label">
<span class="required">*</span>
고객센터 전화번호
</label>
<div class="org-form-input-wrapper">
<div class="org-compound-input">
<select class="org-form-select" id="scAreaCode" required>
<option value="">선택</option>
<option value="국번없음">국번없음</option>
<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>
<input type="hidden" id="scPhoneNumber" th:field="*{scPhoneNumber}">
</div>
<div class="info1 pt28">
<p class="title">
<span class="dot">고객센터 전화번호</span>
</p>
<div class="info_line">
<div class="info_box1">
<span class="input">
<div class="custom_select ct_sc h_inp2" id="scAreaCode">
<div class="select_selected active">선택</div>
<ul class="common_selecttype select_hide select_items ct_sc2">
<li>국번없음</li>
<li>02</li>
<li>031</li>
<li>032</li>
<li>033</li>
<li>041</li>
<li>042</li>
<li>043</li>
<li>044</li>
<li>051</li>
<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>
<!-- Business Registration Certificate Upload -->
<div class="org-form-group">
<label class="org-form-label">
<span class="required">*</span>
사업자 등록증
</label>
<div class="org-form-input-wrapper">
<div class="org-file-upload">
<input type="text" class="org-file-display" id="fileNameDisplay" readonly placeholder="첨부파일을 선택하세요">
<input type="hidden" id="compRegFile" th:field="*{compRegFile}">
<label class="org-file-label" for="files">
<i class="fas fa-upload"></i> 파일선택
<input type="file" id="files" name="files"
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.hwp,.gif,.jpg,.png">
</label>
<button type="button" class="org-file-remove" id="fileRemoveBtn" style="display: none;">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<div class="info1 pt28">
<p class="title tit_add w_tit">
<span class="dot">사업자 등록증</span>
</p>
<div class="info_line info_add">
<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 class="org-file-notice">
<p>첨부파일은 10MB 이내로 등록해 주세요.</p>
<p>문서파일과 이미지 파일만 등록 가능합니다. (pdf, doc, docx, xls, xlsx, ppt, pptx, hwp, gif, jpg, png)</p>
</div>
<div th:if="${#fields.hasErrors('compRegFile')}" class="org-validation-message error">
<span th:errors="*{compRegFile}"></span>
</div>
</div>
</div>
</th:block>
</body>
<th:block layout:fragment="contentScript">
<script th:fragment="orgInfoScript">
function validatePhoneNumber(prefix) {
const areaCode = document.getElementById(`${prefix}AreaCode`).value;
const part1 = document.getElementById(`${prefix}PhoneNumber1`).value.trim();
const part2 = document.getElementById(`${prefix}PhoneNumber2`).value.trim();
// 회사 전화번호 검증
if (!part1 || !part2) {
if (!areaCode || areaCode === '' || !part1 || !part2) {
customPopups.showAlert(`${prefix === 'org' ? '회사' : '고객센터'} 전화번호를 모두 입력해주세요.`);
return false;
}
@@ -289,7 +276,6 @@
}
document.addEventListener('DOMContentLoaded', function() {
activateSelect();
function checkAndSendData(parts, expectedLengths, hiddenInputId, url, paramName) {
let isValid = true;
@@ -305,26 +291,37 @@
if (isValid) {
document.getElementById(hiddenInputId).value = fullValue;
let values = {};
values[paramName] = fullValue;
// CSRF 토큰을 정확하게 가져오기
const csrfToken = document.querySelector('input[name="_csrf"]')?.value ||
document.querySelector('meta[name="_csrf"]')?.content;
document.querySelector('meta[name="_csrf"]')?.content;
htmx.ajax('POST', url, {
values: values,
// AJAX로 검증 요청
$.ajax({
url: url,
type: 'POST',
data: {
[paramName]: fullValue,
_csrf: csrfToken
},
headers: {
'X-XSRF-TOKEN': csrfToken,
'X-Custom-Handler': 'registration-validation'
},
swap: 'none',
target: '#' + hiddenInputId // 명시적 타겟 지정
}).then(function() {
// HTMX 이벤트가 제대로 발생하는지 확인
console.log('HTMX request completed');
}).catch(function(error) {
console.error('HTMX request failed:', error);
success: function(response) {
const validationDiv = document.getElementById(hiddenInputId + '-validation');
if (validationDiv) {
if (response.valid) {
validationDiv.className = 'org-validation-message success';
validationDiv.textContent = response.message || '유효한 번호입니다.';
} 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');
}
// 파일 선택 관련 코드
// File upload handling
const fileInput = document.getElementById('files');
const fileNameInput = document.querySelector('.upload-name');
const fileNameDisplay = document.getElementById('fileNameDisplay');
const compRegFileInput = document.getElementById('compRegFile');
const fileRemoveBtn = document.getElementById('fileRemoveBtn');
fileInput.addEventListener('change', function() {
const files = fileInput.files;
@@ -378,22 +376,20 @@
return;
}
fileNameInput.value = fileNames.join(', ');
fileNameInput.classList.remove('before');
fileNameInput.classList.add('i_trash', 'after');
fileNameDisplay.value = fileNames.join(', ');
fileNameDisplay.classList.add('has-file');
compRegFileInput.value = fileNames.join(', '); // hidden input에 파일 이름들 설정
fileRemoveBtn.style.display = 'block';
}
});
// 파일명 입력란 클릭 시 파일 제거
fileNameInput.addEventListener('click', function() {
if (fileNameInput.classList.contains('i_trash')) {
fileInput.value = '';
fileNameInput.value = '';
compRegFileInput.value = ''; // hidden input 값도 제거
fileNameInput.classList.remove('i_trash', 'after');
fileNameInput.classList.add('before');
}
// File remove button
fileRemoveBtn.addEventListener('click', function() {
fileInput.value = '';
fileNameDisplay.value = '';
compRegFileInput.value = '';
fileNameDisplay.classList.remove('has-file');
fileRemoveBtn.style.display = 'none';
});
function addListeners() {
@@ -404,21 +400,15 @@
document.getElementById('corpRegNo1').addEventListener('input', checkCorporateNumber);
document.getElementById('corpRegNo2').addEventListener('input', checkCorporateNumber);
// 전화번호 조합 함수 추가
// 전화번호 조합 함수
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 part2 = document.getElementById(`${prefix}PhoneNumber2`).value;
if (areaCode === '선택' || !part1 || !part2) return;
if (!areaCode || areaCode === '' || !part1 || !part2) return;
let fullNumber = '';
// 지역번호가 '없음'인 경우에도 접두어로 저장
if (areaCode === '국번없음') {
fullNumber = `국번없음-${part1}-${part2}`;
} else {
fullNumber = `${areaCode}-${part1}-${part2}`;
}
const fullNumber = `${areaCode}-${part1}-${part2}`;
// hidden input에 하이픈이 포함된 값 설정
const hiddenInput = document.getElementById(`${prefix}PhoneNumber`);
@@ -443,12 +433,11 @@
});
// 지역번호 선택 시에도 전화번호 조합
document.querySelector('#orgAreaCode').addEventListener('click', () => combinePhoneNumber('org'));
document.querySelector('#scAreaCode').addEventListener('click', () => combinePhoneNumber('sc'));
document.getElementById('orgAreaCode').addEventListener('change', () => combinePhoneNumber('org'));
document.getElementById('scAreaCode').addEventListener('change', () => combinePhoneNumber('sc'));
}
addListeners();
});
</script>
</th:block>
@@ -1,181 +1,218 @@
<!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>
<th:block th:fragment="orgUserInfoForm">
<div class="info1 pt28">
<p class="title">
<span class="dot">이메일 아이디</span>
</p>
<div class="info_line">
<div class="info_box1">
<input type="text" id="userId" class="common_input_type_1 w_inp"
th:placeholder="#{portalUser.Register.email}">
<span class="special_t">@</span>
<input type="text" id="domain" class="common_input_type_1 w_inp2"
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: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>
<!-- Email ID -->
<div class="org-form-group">
<label class="org-form-label">
<span class="required">*</span>
이메일 아이디
</label>
<div class="org-input-button-group">
<div class="org-compound-input" style="flex: 1;">
<input type="text" id="userId" class="org-form-input"
placeholder="이메일 아이디">
<span class="separator">@</span>
<input type="text" id="domain" class="org-form-input"
placeholder="도메인">
</div>
<button type="button" class="org-btn org-btn-check btn_check_email">
중복체크
</button>
</div>
<input type="hidden" id="loginId" name="loginId" th:value="${loginId}">
<input type="hidden" name="registrationType" th:value="${registrationType}"/>
<input type="hidden" id="registrationScenario" name="registrationScenario" value="new"/>
<div id="email-validation" class="org-validation-message"></div>
</div>
<!-- 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>
</body>
<th:block layout:fragment="contentScript">
<script th:fragment="orgUserInfoScript">
// 이메일 조합 함수
function combineEmail() {
let emailId = document.getElementById('userId').value;
let domain = document.getElementById('domain').value;
let combined = emailId + '@' + domain;
let loginIdInput = document.getElementById('loginId');
if (loginIdInput) {
loginIdInput.value = combined;
<script th:fragment="orgUserInfoScript">
// 이메일 조합 함수
function combineEmail() {
let emailId = document.getElementById('userId').value;
let domain = document.getElementById('domain').value;
let combined = emailId + '@' + domain;
let loginIdInput = document.getElementById('loginId');
if (loginIdInput) {
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) {
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');
customPopups.showAlert(response.message);
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);
}
}
if (checkButton) {
checkButton.style.display = 'none';
}
customPopups.showAlert(response.message);
if (userIdInput && domainInput && loginIdInput) {
userIdInput.disabled = true;
domainInput.disabled = true;
userIdInput.style.backgroundColor = '#e0e0e0';
domainInput.style.backgroundColor = '#e0e0e0';
if (checkButton) {
checkButton.style.display = 'none';
}
const combinedEmail = `${userIdInput.value.trim()}@${domainInput.value.trim()}`;
loginIdInput.value = combinedEmail;
}
if (userIdInput && domainInput && loginIdInput) {
userIdInput.disabled = true;
domainInput.disabled = true;
userIdInput.style.backgroundColor = '#e0e0e0';
domainInput.style.backgroundColor = '#e0e0e0';
if (newUserForm) newUserForm.style.display = 'block';
if (individualConversionForm) individualConversionForm.style.display = 'none';
if (emailChangeForm) emailChangeForm.style.display = 'none';
break;
const combinedEmail = `${userIdInput.value.trim()}@${domainInput.value.trim()}`;
loginIdInput.value = combinedEmail;
}
if (newUserForm) newUserForm.style.display = 'block';
if (individualConversionForm) individualConversionForm.style.display = 'none';
if (emailChangeForm) emailChangeForm.style.display = 'none';
break;
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;
case "conversionOrChange":
customPopups.showEmailValidationPopup(
response.message,
function () {
// "이메일 유지" 선택 시 동작
if (scenarioInput) {
scenarioInput.value = 'retain';
if (!scenarioInput.form) {
document.getElementById('registerForm').appendChild(scenarioInput);
}
}
} else if (response && response.message) {
customPopups.showAlert(response.message);
} else {
customPopups.showAlert('알 수 없는 오류가 발생했습니다.');
}
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);
} else {
customPopups.showAlert('알 수 없는 오류가 발생했습니다.');
}
}
// 이메일 입력 필드에 이벤트 리스너 추가
document.addEventListener('DOMContentLoaded', function () {
let userIdInput = document.getElementById("userId");
let domainInput = document.getElementById("domain");
if (userIdInput) userIdInput.addEventListener("input", combineEmail);
if (domainInput) domainInput.addEventListener("input", combineEmail);
// Email check button click handler
document.addEventListener('DOMContentLoaded', function () {
let userIdInput = document.getElementById("userId");
let domainInput = document.getElementById("domain");
const checkButton = document.querySelector('.btn_check_email');
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>
</html>
</html>
@@ -1,501 +1,515 @@
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/kbank_base_layout}">
layout:decorate="~{layout/kjbank_signup_layout}">
<body>
<section layout:fragment="contentFragment" class="content">
<script th:src="@{/js/htmx.min.js}"></script>
<div class="content_wrap">
<div class="sub_title2">
<h2 class="title add">법인회원가입</h2>
</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 layout:fragment="title">
<div class="org-page-title-banner">
<img th:src="@{/img/user_register_title.png}" alt="법인회원가입" class="title-image">
<h1>법인회원가입</h1>
</div>
</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">
<script th:if="${error}" th:inline="javascript">
$(document).ready(function () {
customPopups.showAlert([[${error}]]);
<script th:if="${error}" th:inline="javascript">
$(document).ready(function () {
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에서 가져오도록 수정
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);
});
// 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});
// 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});
}
// 시나리오별 필수 필드 정의
const requiredFieldsByScenario = {
new: {
user: ['loginId', 'userName', 'password', 'password2', 'mobileNumber', 'authNumber'],
org: ['compRegNo', 'corpRegNo', 'orgName', 'ceoName', 'orgAddr', 'orgSectors', 'orgIndustryType', 'orgPhoneNumber', 'scPhoneNumber', 'serviceName', 'compRegFile', 'files']
},
retain: {
user: ['loginId', 'passwordConfirmIndividual'],
org: ['compRegNo', 'corpRegNo', 'orgName', 'ceoName', 'orgAddr', 'orgSectors', 'orgIndustryType', 'orgPhoneNumber', 'scPhoneNumber', 'serviceName', 'compRegFile', 'files']
},
change: {
user: ['loginId', 'passwordConfirmEmailChange', 'newLoginId'],
org: ['compRegNo', 'corpRegNo', 'orgName', 'ceoName', 'orgAddr', 'orgSectors', 'orgIndustryType', 'orgPhoneNumber', 'scPhoneNumber', 'serviceName', 'compRegFile', 'files']
}
};
// 시나리오별 필수 필드 정의
const requiredFieldsByScenario = {
new: {
user: ['loginId', 'userName', 'password', 'password2', 'mobileNumber', 'authNumber'],
org: ['compRegNo', 'corpRegNo', 'orgName', 'ceoName', 'orgAddr', 'orgSectors', 'orgIndustryType', 'orgPhoneNumber', 'scPhoneNumber', 'serviceName', 'compRegFile', 'files']
},
retain: {
user: ['loginId', 'passwordConfirmIndividual'],
org: ['compRegNo', 'corpRegNo', 'orgName', 'ceoName', 'orgAddr', 'orgSectors', 'orgIndustryType', 'orgPhoneNumber', 'scPhoneNumber', 'serviceName', 'compRegFile', 'files']
},
change: {
user: ['loginId', 'passwordConfirmEmailChange', 'newLoginId'],
org: ['compRegNo', 'corpRegNo', 'orgName', 'ceoName', 'orgAddr', 'orgSectors', 'orgIndustryType', 'orgPhoneNumber', 'scPhoneNumber', 'serviceName', 'compRegFile', 'files']
}
};
// 이메일 유효성 검사 함수
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
// 이메일 유효성 검사 함수
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
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() {
const form = document.createElement('form');
form.enctype = 'multipart/form-data';
form.method = 'POST';
form.action = '/signup/portalOrg';
form.style.display = 'none';
// Add confirmPassword manually based on the scenario
if (registrationScenario === 'retain') {
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 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) {
console.error('Unknown scenario');
return form;
}
if (document.getElementById('privacyCollect')) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'privacyCollect';
input.value = document.getElementById('privacyCollect').checked;
form.appendChild(input);
}
// 시나리오 값 추가
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);
}
// 시스템 필드 추가
const csrfToken = document.querySelector('input[name="_csrf"]');
if (csrfToken) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = '_csrf';
input.value = csrfToken.value;
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);
}
});
const registrationType = document.querySelector('input[name="registrationType"]');
if (registrationType) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'registrationType';
input.value = registrationType.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);
}
}
});
console.log('Collected Form Data for scenario:',
document.getElementById('registrationScenario')?.value || registrationScenario);
return form;
}
// Add confirmPassword manually based on the scenario
if (registrationScenario === 'retain') {
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);
}
}
// 시나리오별 유효성 검사
function validateFormByScenario() {
// 공통 필드 추가
if (document.getElementById('termsOfUse')) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'termsOfUse';
input.value = document.getElementById('termsOfUse').checked;
form.appendChild(input);
}
const currentScenario = document.getElementById('registrationScenario')?.value || 'new';
console.log('Registration Scenario:', currentScenario);
if (document.getElementById('privacyCollect')) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'privacyCollect';
input.value = document.getElementById('privacyCollect').checked;
form.appendChild(input);
}
if (!currentScenario) {
console.log('Unknown scenario');
return false;
}
// 시스템 필드 추가
const csrfToken = document.querySelector('input[name="_csrf"]');
if (csrfToken) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = '_csrf';
input.value = csrfToken.value;
form.appendChild(input);
}
switch (registrationScenario) {
case 'new':
const loginIdElement = document.getElementById('loginId');
const userNameElement = document.getElementById('userName');
const registrationType = document.querySelector('input[name="registrationType"]');
if (registrationType) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'registrationType';
input.value = registrationType.value;
form.appendChild(input);
}
const isEmailValid = loginIdElement ? isValidEmail(loginIdElement.value) : false;
const hasUserName = userNameElement ? userNameElement.value.trim() !== '' : false;
const isPasswordValid = document.getElementById('isPasswordValid').value.trim() === 'true';
const isPasswordMatch = document.getElementById('isPasswordMatch').value.trim() === 'true';
console.log('Collected Form Data for scenario:',
document.getElementById('registrationScenario')?.value || registrationScenario);
return form;
}
console.log('New User Validation:', {
email: loginIdElement?.value,
isEmailValid,
isPasswordValid,
isPasswordMatch,
userName: userNameElement?.value,
hasUserName,
isAuthVerified,
requiredFields: currentScenario
});
return isEmailValid && isPasswordValid && isPasswordMatch && hasUserName && isAuthVerified;
// 시나리오별 유효성 검사
function validateFormByScenario() {
case 'retain':
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('Registration Scenario:', currentScenario);
console.log('Retain User Validation:', {
email: retainLoginIdElement?.value,
isEmailValid: retainLoginIdElement ? isValidEmail(retainLoginIdElement.value) : false,
passwordLength: passwordConfirmElement?.value.length,
hasPassword,
requiredFields: currentScenario
});
return retainLoginIdElement && isValidEmail(retainLoginIdElement.value) && hasPassword;
if (!currentScenario) {
console.log('Unknown scenario');
return false;
}
case 'change':
const changeLoginIdElement = document.getElementById('loginId');
const newLoginIdElement = document.getElementById('newLoginId');
const changePasswordElement = document.getElementById('passwordConfirmEmailChange');
const hasChangePassword = changePasswordElement ? changePasswordElement.value.length >= 8 : false;
switch (registrationScenario) {
case 'new':
const loginIdElement = document.getElementById('loginId');
const userNameElement = document.getElementById('userName');
console.log('Change Email Validation:', {
currentEmail: changeLoginIdElement?.value,
newEmail: newLoginIdElement?.value,
hasPassword: hasChangePassword,
requiredFields: currentScenario
});
return changeLoginIdElement && newLoginIdElement &&
isValidEmail(changeLoginIdElement.value) &&
hasChangePassword &&
isValidEmail(newLoginIdElement.value);
const isEmailValid = loginIdElement ? isValidEmail(loginIdElement.value) : false;
const hasUserName = userNameElement ? userNameElement.value.trim() !== '' : false;
const isPasswordValid = document.getElementById('isPasswordValid').value.trim() === 'true';
const isPasswordMatch = document.getElementById('isPasswordMatch').value.trim() === 'true';
default:
console.log('Unknown scenario');
return false;
}
}
console.log('New User Validation:', {
email: loginIdElement?.value,
isEmailValid,
isPasswordValid,
isPasswordMatch,
userName: userNameElement?.value,
hasUserName,
isAuthVerified,
requiredFields: currentScenario
});
return isEmailValid && isPasswordValid && isPasswordMatch && hasUserName && isAuthVerified;
// 공통 필드 유효성 검사
function validateCommonFields() {
case 'retain':
const passwordConfirmElement = document.getElementById('passwordConfirmIndividual');
const hasPassword = passwordConfirmElement ? passwordConfirmElement.value.length >= 8 : false;
const retainLoginIdElement = document.getElementById('loginId');
if(!validatePhoneNumber('org') || !validatePhoneNumber('sc')) {
return false;
}
console.log('Retain User Validation:', {
email: retainLoginIdElement?.value,
isEmailValid: retainLoginIdElement ? isValidEmail(retainLoginIdElement.value) : false,
passwordLength: passwordConfirmElement?.value.length,
hasPassword,
requiredFields: currentScenario
});
return retainLoginIdElement && isValidEmail(retainLoginIdElement.value) && hasPassword;
const validations = {
termsOfUse: $('#termsOfUse').prop('checked'),
privacyPolicy: $('#privacyCollect').prop('checked'),
compRegNo: $('#compRegNo').val().trim() !== '',
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() !== ''
};
case 'change':
const changeLoginIdElement = document.getElementById('loginId');
const newLoginIdElement = document.getElementById('newLoginId');
const changePasswordElement = document.getElementById('passwordConfirmEmailChange');
const hasChangePassword = changePasswordElement ? changePasswordElement.value.length >= 8 : false;
console.log('Common Fields Validation:', validations);
return Object.values(validations).every(value => value === true);
}
console.log('Change Email Validation:', {
currentEmail: changeLoginIdElement?.value,
newEmail: newLoginIdElement?.value,
hasPassword: hasChangePassword,
requiredFields: currentScenario
});
return changeLoginIdElement && newLoginIdElement &&
isValidEmail(changeLoginIdElement.value) &&
hasChangePassword &&
isValidEmail(newLoginIdElement.value);
// 전체 폼 유효성 검사
function validateForm() {
const scenarioValidation = validateFormByScenario();
const commonValidation = validateCommonFields();
default:
console.log('Unknown scenario');
return false;
}
}
console.log('Form validation result:', {
scenarioValidation,
commonValidation,
registrationScenario
});
// 공통 필드 유효성 검사
function validateCommonFields() {
return scenarioValidation && commonValidation;
}
if(!validatePhoneNumber('org') || !validatePhoneNumber('sc')) {
return false;
}
let isSubmitting = false;
const validations = {
termsOfUse: $('#termsOfUse').prop('checked'),
privacyPolicy: $('#privacyCollect').prop('checked'),
compRegNo: $('#compRegNo').val().trim() !== '',
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() !== ''
};
// 등록 버튼 클릭 이벤트
btn_register.addEventListener('click', function (event) {
event.preventDefault();
event.stopPropagation();
console.log('Common Fields Validation:', validations);
return Object.values(validations).every(value => value === true);
}
// Prevent resubmission
if (isSubmitting) {
console.log('Form is already being submitted...');
return;
}
// 전체 폼 유효성 검사
function validateForm() {
const scenarioValidation = validateFormByScenario();
const commonValidation = validateCommonFields();
isSubmitting = true;
btn_register.disabled = true;
console.log('Form validation result:', {
scenarioValidation,
commonValidation,
registrationScenario
});
// Show loading overlay
document.getElementById('loadingOverlay').classList.add('active');
return scenarioValidation && commonValidation;
}
try {
form.classList.add('was-validated');
if (agreementForm) {
agreementForm.classList.add('was-validated');
}
let isSubmitting = false;
console.log('Validating form...');
// 등록 버튼 클릭 이벤트
btn_register.addEventListener('click', function (event) {
event.preventDefault();
event.stopPropagation();
if (validateForm()) {
console.log('Form validation passed, collecting form data...');
const form = collectFormData();
document.body.appendChild(form);
form.submit();
} else {
console.log('Form validation failed');
// Prevent resubmission
if (isSubmitting) {
console.log('Form is already being submitted...');
return;
}
// 현재 상태 출력
const scenarioValidation = validateFormByScenario();
const commonValidation = validateCommonFields();
isSubmitting = true;
btn_register.disabled = true;
try {
form.classList.add('was-validated');
if (agreementForm) {
agreementForm.classList.add('was-validated');
}
const isPasswordValid = document.getElementById('isPasswordValid')?.value.trim() === 'true';
const isPasswordMatch = document.getElementById('isPasswordMatch')?.value.trim() === 'true';
console.log('Validating form...');
console.log('Validation details:', {
scenarioValidation,
commonValidation,
registrationScenario,
isAuthVerified,
isPasswordValid,
isPasswordMatch
});
if (validateForm()) {
console.log('Form validation passed, collecting form data...');
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;
}
let errorMessage = '모든 필수 항목을 입력하고 약관에 동의해주세요.'
customPopups.showAlert(errorMessage);
// 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) {
if (response.valid) {
customPopups.showAlert(response.message || '신규 이메일 정보입니다.');
} else {
customPopups.showAlert(response.message || '이미 사용 중인 이메일입니다.');
}
// 신규 이메일 중복 체크 응답 처리 함수
function handleNewEmailValidationResponse(response) {
if (response.valid) {
customPopups.showAlert(response.message || '신규 이메일 정보입니다.');
} else {
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) {
const targetId = event.detail.target.id;
let response;
// 커스텀 이벤트 리스너
document.addEventListener('htmxResponse', function (event) {
const {targetId, response} = event.detail;
try {
response = JSON.parse(event.detail.xhr.response);
} catch (e) {
console.error('응답 JSON 파싱 실패:', e);
customPopups.showAlert('서버 응답을 처리할 수 없습니다.');
return;
}
if (targetId === 'compRegNo') {
customPopups.showAlert(response.message || (response.valid ? '유효한 사업자등록번호입니다.' : '유효하지 않은 사업자등록번호입니다.'));
} else if (targetId === 'corpRegNo') {
customPopups.showAlert(response.message || (response.valid ? '유효한 법인등록번호입니다.' : '유효하지 않은 법인등록번호입니다.'));
}
});
// 비밀번호 관련 검증은 컴포넌트의 로직을 따르도록 return
if (targetId === 'password-validation' || targetId === 'password-match-validation') {
return;
}
// 인증 완료 이벤트 리스너
document.addEventListener('authVerified', function () {
isAuthVerified = true;
});
// 나머지 요청들에 대해서만 처리
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);
}
});
// 커스텀 이벤트 리스너
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>
// 취소 버튼 클릭 시
document.getElementById('cancelButton').addEventListener('click', function () {
location.href = '/login';
});
});
</script>
</th:block>
</body>
</html>
@@ -5,53 +5,70 @@
layout:decorate="~{layout/base_layout}">
<body>
<th:block th:fragment="agreementContent(termsOfUse, privacyCollect)">
<div class="terms_box t_top">
<p>약관 동의</p>
<!-- Agreement Section Header -->
<div class="org-section-header org-section-header--agreement">
<h3>약관 동의</h3>
<span class="required-badge">필수 동의</span>
</div>
<form id="agreementForm">
<div class="total_wrap">
<span class="inp_check b_tit">
<input type='checkbox' value='selectall' id="agree_all"/>
<label for="agree_all">전체동의</label>
</span>
<div class="total_type">
<div class="suv_ck">
<ul>
<li>
<span class="inp_check s_tit">
<input type='checkbox' name='termsOfUse' id="termsOfUse" required/>
<label for="termsOfUse">
<span>[필수]</span>「API Portal」 서비스 이용약관
</label>
</span>
<a class="toggle-btn" data-target="termsOfUseContent"></a>
<div id="termsOfUseContent" class="txt_wrap scrollbar" style="display:none;">
<div th:utext="${termsOfUse?.contents ?: '이용약관 내용을 불러올 수 없습니다.'}"></div>
</div>
</li>
</ul>
<form id="agreementForm" class="agreement-form">
<!-- Agree All Checkbox -->
<div class="agreement-all-section">
<label class="agreement-checkbox-label">
<input type="checkbox" id="agree_all" class="agreement-checkbox-input">
<span class="agreement-checkbox-custom"></span>
<span class="agreement-checkbox-text agreement-all-text">전체동의</span>
</label>
</div>
<!-- Divider -->
<div class="agreement-divider"></div>
<!-- Individual Agreements -->
<div class="agreement-items-list">
<!-- Terms of Use -->
<div class="agreement-item-row">
<label class="agreement-checkbox-label">
<input type="checkbox" name="termsOfUse" id="termsOfUse" class="agreement-checkbox-input" required>
<span class="agreement-checkbox-custom"></span>
<span class="agreement-checkbox-text">
<span class="agreement-required">[필수]</span>
<span class="agreement-title">이용약관 동의</span>
</span>
</label>
<button type="button" class="agreement-toggle-icon" data-target="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 class="suv_ck">
<ul>
<li>
<span class="inp_check s_tit">
<input type='checkbox' name='privacyCollect' id="privacyCollect" required/>
<th:block th:switch="${registrationType}">
<label for="privacyCollect" th:case="'personal'">
<span>[필수]</span> Kbank API Portal 회원가입을 위한 개인정보 수집•이용 동의서 (개인회원용)
</label>
<label for="privacyCollect" th:case="'corporate'">
<span>[필수]</span> Kbank API Portal 회원가입을 위한 개인정보 수집•이용 동의서 (법인회원용)
</label>
</th:block>
</span>
<a class="toggle-btn" data-target="privacyCollectContent"></a>
<div id="privacyCollectContent" class="txt_wrap scrollbar" style="display:none;">
<div th:utext="${privacyCollect?.contents ?: '개인정보수집동의서 내용을 불러올 수 없습니다.'}"></div>
</div>
</li>
</ul>
</div>
<!-- Privacy Policy -->
<div class="agreement-item-row">
<label class="agreement-checkbox-label">
<input type="checkbox" name="privacyCollect" id="privacyCollect" class="agreement-checkbox-input" required>
<span class="agreement-checkbox-custom"></span>
<th:block th:switch="${registrationType}">
<span class="agreement-checkbox-text" th:case="'personal'">
<span class="agreement-required">[필수]</span>
<span class="agreement-title">개인정보 수집 및 이용 동의</span>
</span>
<span class="agreement-checkbox-text" th:case="'corporate'">
<span class="agreement-required">[필수]</span>
<span class="agreement-title">개인정보 수집 및 이용 동의</span>
</span>
</th:block>
</label>
<button type="button" class="agreement-toggle-icon" data-target="privacyCollectContent">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<div id="privacyCollectContent" class="agreement-content" style="display:none;">
<div class="agreement-scroll">
<div th:utext="${privacyCollect?.contents ?: '개인정보수집동의서 내용을 불러올 수 없습니다.'}"></div>
</div>
</div>
</div>
@@ -61,49 +78,56 @@
<script th:fragment="agreementScript">
document.addEventListener('DOMContentLoaded', function () {
const agreeAll = document.getElementById('agree_all');
const agreementChecks = document.querySelectorAll('.inp_check.s_tit input[type="checkbox"]');
const toggleBtns = document.querySelectorAll('.toggle-btn');
const agreementChecks = document.querySelectorAll('.agreement-checkbox-input[required]');
const toggleIcons = document.querySelectorAll('.agreement-toggle-icon');
// 전체 동의 체크박스 이벤트
agreeAll.addEventListener('change', function () {
agreementChecks.forEach(check => {
check.checked = this.checked;
if (agreeAll) {
agreeAll.addEventListener('change', function () {
agreementChecks.forEach(check => {
check.checked = this.checked;
});
});
});
}
// 개별 체크박스 이벤트
agreementChecks.forEach(check => {
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 => {
btn.addEventListener('click', function (e) {
// 토글 아이콘 이벤트
toggleIcons.forEach(icon => {
icon.addEventListener('click', function (e) {
e.preventDefault();
const targetId = this.getAttribute('data-target');
const targetContent = document.getElementById(targetId);
const parentSection = this.closest('.suv_ck');
const parentRow = this.closest('.agreement-item-row');
// 현재 섹션이 이미 열려있는지 확인
const isCurrentlyOpen = targetContent.style.display === 'block';
document.querySelectorAll('.txt_wrap').forEach(content => {
// 모든 섹션 닫기
document.querySelectorAll('.agreement-content').forEach(content => {
content.style.display = 'none';
});
document.querySelectorAll('.toggle-btn').forEach(button => {
document.querySelectorAll('.agreement-toggle-icon').forEach(button => {
button.classList.remove('active');
});
document.querySelectorAll('.suv_ck').forEach(section => {
section.classList.remove('active');
document.querySelectorAll('.agreement-item-row').forEach(item => {
item.classList.remove('active');
});
// 현재 섹션이 닫혀있었다면 열기
if (!isCurrentlyOpen) {
targetContent.style.display = 'block';
this.classList.add('active');
parentSection.classList.add('active');
if (parentRow) {
parentRow.classList.add('active');
}
setTimeout(() => {
targetContent.scrollIntoView({
@@ -1,65 +1,76 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
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}">
layout:decorate="~{layout/kjbank_signup_layout}">
<body>
<section layout:fragment="contentFragment" class="content">
<script th:src="@{/js/htmx.min.js}"></script>
<div class="content_wrap">
<div class="sub_title2">
<h2 class="title add" th:if="${!isInvited}">개인회원가입</h2>
<h2 class="title add" th:if="${isInvited}">법인회원가입</h2>
</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>
<section layout:fragment="title">
<div class="org-page-title-banner">
<img th:src="@{/img/user_register_title.png}" alt="개인회원가입" class="title-image">
<h1 th:text="${!isInvited ? '개인회원가입' : '법인회원가입'}">개인회원가입</h1>
</div>
</section>
<div class="form_type" th:if="${isInvited}">
<div class="info1">
<p class="title w_tit">
<span>법인명</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4">
<input type="text" name="companyName" class="common_input_type_1"
th:classappend="${isInvited ? 'input-readonly' : ''}" th:value="${orgName}" readonly>
</div>
</div>
</div>
</div>
<!-- end 법인 정보 섹션, 초대받은 경우에만 -->
<div class="terms_box">
<p>기본 정보</p>
<span class="dot">필수 입력란입니다</span>
</div>
<div class="form_type" th:if="${registrationType == isInvited ? 'corporate' :'personal'}">
<th:block th:replace="~{apps/register/components/commonUserInfoForm :: commonUserInfo}"></th:block>
<th:block th:replace="~{apps/register/components/newUserInfoForm :: newUserForm}"></th:block>
</div>
<div 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>
<section layout:fragment="contentFragment">
<div class="org-register-page">
<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>
<!-- Info Notice -->
<div class="org-info-notice" th:if="${!isInvited}">
<ul>
<li>서비스 또는 API 사용을 원하실 경우 법인회원 승인 후 이용하실 수 있습니다.</li>
</ul>
</div>
<!-- Agreement Section -->
<th:block th:replace="~{apps/register/userAgreementContent :: agreementContent(${termsOfUse}, ${privacyCollect})}"></th:block>
<form name="portalUser" id="registerForm" role="form" th:action="@{/signup/portalUser}"
th:object="${portalUser}" method="post">
<!-- Corporate Information Section (invited users only) -->
<div th:if="${isInvited}">
<div class="org-section-header org-section-header--agreement">
<h3>법인 정보</h3>
</div>
<div class="org-form-group">
<label class="org-form-label">법인명</label>
<div class="org-form-input-wrapper">
<input type="text" name="companyName" class="org-form-input" th:value="${orgName}" readonly>
</div>
</div>
</div>
<!-- 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>
</section>
<th:block layout:fragment="contentScript">
<script th:if="${error}" th:inline="javascript">
@@ -70,128 +81,119 @@
<script th:replace="~{apps/register/userAgreementContent :: agreementScript}"></script>
<script th:replace="~{apps/register/components/commonUserInfoForm :: commonUserInfoScript}"></script>
<script th:replace="~{apps/register/components/newUserInfoForm :: newUserScript}"></script>
<script>
let form = document.getElementById('registerForm');
let agreementForm = document.getElementById('agreementForm');
let btn_register = document.querySelector('.btn_register');
let isAuthVerified = false;
<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;
// 인증 완료 이벤트 리스너
document.addEventListener('authVerified', function() {
isAuthVerified = true;
});
// 인증 완료 이벤트 리스너
document.addEventListener('authVerified', function() {
isAuthVerified = true;
});
// 이메일 유효성 검사 함수
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])?)*$/;
return safeEmailRegex.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;
// 이메일 유효성 검사 함수
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
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');
agreementForm.classList.add('was-validated');
return isValidEmail(loginIdElement.val()) &&
isPasswordValid &&
isPasswordMatch &&
userNameElement.val().trim() !== '' &&
termsOfUseElement.prop('checked') &&
privacyCollectElement.prop('checked') &&
mobileNumberElement.val().trim() !== '' &&
isAuthVerified;
}
if (checkFormValidity() && form.checkValidity() && agreementForm.checkValidity()) {
// 약관 동의 폼의 데이터를 hidden input으로 추가
let agreementFormData = new FormData(agreementForm);
for (let pair of agreementFormData.entries()) {
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);
}
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;
}
// 불필요한 hidden input 제거
const unnecessaryFields = ['isPasswordValid', 'isPasswordMatch'];
unnecessaryFields.forEach(fieldName => {
const field = form.querySelector(`input[name="${fieldName}"]`);
if (field) {
field.remove();
isSubmitting = true;
btn_register.disabled = true;
// Show loading overlay
document.getElementById('loadingOverlay').classList.add('active');
try {
form.classList.add('was-validated');
if (agreementForm) {
agreementForm.classList.add('was-validated');
}
});
// authCompletedYn 값 설정
const authCompletedField = document.createElement('input');
authCompletedField.type = 'hidden';
authCompletedField.name = 'authCompletedYn';
authCompletedField.value = isAuthVerified ? 'Y' : 'N';
form.appendChild(authCompletedField);
if (checkFormValidity() && form.checkValidity() && (agreementForm ? agreementForm.checkValidity() : true)) {
// 약관 동의 폼의 데이터를 hidden input으로 추가
if (agreementForm) {
let agreementFormData = new FormData(agreementForm);
for (let pair of agreementFormData.entries()) {
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();
} else {
customPopups.showAlert('모든 필수 항목을 입력하고 약관에 동의해주세요.');
isSubmitting = false;
btn_register.disabled = false;
}
});
// 불필요한 hidden input 제거
const unnecessaryFields = ['isPasswordValid', 'isPasswordMatch'];
unnecessaryFields.forEach(fieldName => {
const field = form.querySelector(`input[name="${fieldName}"]`);
if (field) {
field.remove();
}
});
// 취소버튼 클릭시
document.getElementById('cancelButton').addEventListener('click', function() {
location.href = '/login';
});
// authCompletedYn 값 설정
const authCompletedField = document.createElement('input');
authCompletedField.type = 'hidden';
authCompletedField.name = 'authCompletedYn';
authCompletedField.value = isAuthVerified ? 'Y' : 'N';
form.appendChild(authCompletedField);
// 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;
}
// 커스텀 헤더를 체크하여 등록 검증 요청 처리
if (event.detail.xhr.getResponseHeader('X-Custom-Handler') === 'registration-validation') {
if (response && response.message) {
if (response.status === 'SUCCESS') {
customPopups.showSuccess(response.message);
form.submit();
} 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') {
// commonUserInfoForm의 함수 호출
handleEmailValidationResponse(response);
} else if (targetId === 'password-validation' || targetId === 'password-match-validation') {
// 비밀번호 검증은 컴포넌트에서 처리하도록 함
return;
}
// 취소 버튼 클릭 시
document.getElementById('cancelButton').addEventListener('click', function() {
location.href = '/login';
});
});
</script>
</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>