회원가입 디자인 적용

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

After

Width:  |  Height:  |  Size: 808 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 990 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

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