diff --git a/src/App.tsx b/src/App.tsx index 309f6e1..a2a506f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,7 +5,7 @@ import { BlockEditPage } from './Pages/BlockEditPage.tsx'; import { CreateChannel } from './Pages/CreateChannel'; import { DeploymentListPage } from './Pages/DeploymentListPage.tsx'; import { Home } from './Pages/Home'; -import { Join } from './Pages/Join'; +import { Join } from './Pages/Join.tsx'; import { Login } from './Pages/Login'; import { LoginCallback } from './Pages/LoginCallback'; import { OnBoarding } from './Pages/Onboarding'; diff --git a/src/Pages/CreateChannel.tsx b/src/Pages/CreateChannel.tsx index 07f52fd..eb414a3 100644 --- a/src/Pages/CreateChannel.tsx +++ b/src/Pages/CreateChannel.tsx @@ -5,6 +5,9 @@ import { channelApi, userApi } from '../api'; import { LocalStorageUtils } from '../utils/LocalStorageUtils.ts'; import { useUserContextQuery } from '../api/queries.tsx'; import { useState } from 'react'; +import { InputRuled } from '../components/InputRuled.tsx'; +import { reject } from 'lodash'; +import { validateInput, validationConfig } from '../utils/validators.ts'; type CreateChannelType = { channelName: string; @@ -28,7 +31,7 @@ export function CreateChannel() { LocalStorageUtils.setSelectedChannelId( userResponse.data.channelPermissionList[0].channelId, ); - navigate('/home', {state: {userContextReloadKey: 'true'}}); + navigate('/home', { state: { userContextReloadKey: 'true' } }); } })(); }; @@ -55,15 +58,28 @@ export function CreateChannel() { label="채널 이름" name={'channelName'} - rules={[{ required: true, message: '채널 이름을 입력해 주세요!' }]} + required={true} + rules={[ + { + validator: (_, value) => { + return validateInput({ value, ...validationConfig.channelName }); + } + } + ]} > - label="채널 설명" name={'channelDescription'} - rules={[{ required: true, message: '채널 설명을 입력해 주세요!' }]} + required={true} + rules={[ + { + validator: (_, value) => { + return validateInput({ value, ...validationConfig.channelDescription }); + } + } + ]} > @@ -71,7 +87,14 @@ export function CreateChannel() { label="채널 채널 프로필 이미지 URL" name={'channelProfileImageUrl'} - rules={[{ required: false, message: '비밀번호를 입력해주세요!' }]} + required={true} + rules={[ + { + validator: (_, value) => { + return validateInput({ value, ...validationConfig.channelProfileImg }); + } + } + ]} > diff --git a/src/Pages/Home.tsx b/src/Pages/Home.tsx index af121ec..5a938e8 100644 --- a/src/Pages/Home.tsx +++ b/src/Pages/Home.tsx @@ -17,6 +17,7 @@ import { sketchApi } from '../api'; import { MainLayout } from '../components/MainLayout.tsx'; import { useChannelNavigationContext } from '../components/providers/ChannelNavigationProvider.tsx'; import { SketchProjection } from '../types/SketchProjection.ts'; +import { validateInput, validationConfig } from '../utils/validators.ts'; type CreateSketchType = { name: string; @@ -25,7 +26,7 @@ type CreateSketchType = { export function Home() { const location = useLocation(); - const state = {...location.state}; + const state = { ...location.state }; const userContextReloadKey = state.userContextReloadKey; return ( @@ -144,7 +145,8 @@ export function SketchListView({ id={'createSketchForm'} name="basic" onFinish={(value) => { - (async () => { + ( + async () => { await sketchApi.createSketchAsync(currentChannel.channelId, { name: value.name, description: value.description, @@ -163,21 +165,27 @@ export function SketchListView({ label="스케치 이름" name={'name'} - rules={[ - { required: true, message: '스케치 이름을 입력해 주세요!' }, - ]} + required={true} + rules = {[{ + validator: (_, value) =>{ + return validateInput({value: value, ...validationConfig.sketchName}); + } + }]} > - + + label="스케치 설명" name={'description'} - rules={[ - { required: true, message: '스케치 설명을 입력해 주세요!' }, - ]} + required={true} + rules={[{ + validator: (_, value) =>{ + return validateInput({value: value, ...validationConfig.sketchDescription}); + }}]} > - + diff --git a/src/Pages/Join.tsx b/src/Pages/Join.tsx index ec8eba8..e13f75c 100644 --- a/src/Pages/Join.tsx +++ b/src/Pages/Join.tsx @@ -1,9 +1,10 @@ import { Button, Card, Form, Input } from 'antd'; import { useNavigate } from 'react-router-dom'; -import { authApi } from '../api'; -import { CredentialProvider } from '../libs/core-api/api'; +import { authApi } from '../api/index.ts'; +import { CredentialProvider } from '../libs/core-api/api/index.ts'; import { LocalStorageUtils } from '../utils/LocalStorageUtils.ts'; +import { validateInput, validationConfig } from '../utils/validators.ts'; type RegistrationFormType = { userName: string; @@ -54,9 +55,9 @@ export function Join() { label="사용자 이름" name={'userName'} - rules={[ - { required: true, message: '사용자 이름을 입력해 주세요!' }, - ]} + rules={[{validator:(_,value) =>{ + return validateInput({value: value, ...validationConfig.userName}); + }}]} > @@ -64,7 +65,11 @@ export function Join() { label="사용자 이메일" name={'userEmail'} - rules={[{ required: true, message: '비밀번호를 입력해주세요!' }]} + rules={[ + {validator:(_,value) =>{ + return validateInput({value: value, ...validationConfig.userEmail}); + }} + ]} > diff --git a/src/components/InputRuled.tsx b/src/components/InputRuled.tsx new file mode 100644 index 0000000..42a4069 --- /dev/null +++ b/src/components/InputRuled.tsx @@ -0,0 +1,69 @@ +import { Input, Typography, message } from "antd"; +import { InputStatus } from "antd/es/_util/statusUtils"; +import { max, set } from "lodash"; +import { useEffect, useState } from "react"; +import { CheckInputType, InputRuledProps, checkInput } from "../utils/InputUtils"; +import { SizeType } from "antd/es/config-provider/SizeContext"; + +// InputRuled 컴포넌트는 antd Input 컴포넌트를 상속받아서 사용합니다. +// Input, Form.Item의 Input 모두에서 사용 가능합니다. + +// required한 경우, min_length를 최소 1 이상으로 설정해야합니다. +// required하지 않은 경우 min_length를 0으로 설정하면 됩니다. + +// 모든 조건을 충족하는 경우를 ''로 표시합니다. + + + +// 에러 메시지 출력 +export const ErrorMessage = ({message}:{message?:string}) => { + return ( +
+ + {message} + +
+ ); +} + +export const InputRuled = ({ value = '',callback, style, size,...props }: InputRuledProps&{callback?: (value: string) => void, style?:React.CSSProperties, size?:SizeType}) => { + const [inputValue, setInputValue] = useState(value); + + // input 상태 + const [status, setStatus] = useState({ status: '', message: '' }); + + const handleInput = (event: React.ChangeEvent) => { + const input = event.target.value; + + // input 무결성 확인 및 결과에 따른 status 설정 + setStatus(checkInput({ ...props, value: input })); + console.log(input,status); + + // length가 max를 초과하지 않는 경우에만 input value 설정 + if(status.errorType !== 'length_max'){ + setInputValue(input); + } + + if(status.status === '' && callback){ + callback(inputValue); + } + }; + + return ( + <> + + + + ); +}; + diff --git a/src/components/blocks/WebServerBlockNode.tsx b/src/components/blocks/WebServerBlockNode.tsx index 1949a14..0166c7e 100644 --- a/src/components/blocks/WebServerBlockNode.tsx +++ b/src/components/blocks/WebServerBlockNode.tsx @@ -21,7 +21,7 @@ export type WebServerBlockNodeProps = CommonBlockProps & { imageTags: string; username: string | undefined; secrets: string | undefined; - containerPort: number; + containerPort?: number; }; connectionMetadata: { dbRef: string; diff --git a/src/components/blocks/editor/DatabaseBlockEditor.tsx b/src/components/blocks/editor/DatabaseBlockEditor.tsx index 16a5465..a20dc15 100644 --- a/src/components/blocks/editor/DatabaseBlockEditor.tsx +++ b/src/components/blocks/editor/DatabaseBlockEditor.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { DatabaseBlockNodeProps } from '../DatabaseBlockNode.tsx'; import { CommonBlockNodeContentProps } from './BlockNodeEditDrawer.tsx'; +import { validateInput, validationConfig } from '../../../utils/validators.ts'; const { Option } = Select; export function DatabaseBlockEditor({ @@ -33,7 +34,12 @@ export function DatabaseBlockEditor({ label="리소스 이름" name={'blockTitle'} - rules={[{ required: true, message: '리소스 이름은 필수 필드입니다!' }]} + rules={[ + {validator(_, value){ + return validateInput({value, ...validationConfig.blockTitle}) + }} + ]} + initialValue={node.data.blockTitle} > @@ -43,7 +49,9 @@ export function DatabaseBlockEditor({ label="리소스 설명" name={'blockDescription'} rules={[ - { required: true, message: '리소스에 대한 설명은 필수 입니다!' }, + {validator(_, value){ + return validateInput({value, ...validationConfig.blockDescription})} + } ]} initialValue={node.data.blockDescription} > diff --git a/src/components/blocks/editor/VirtualMachineBlockEditor.tsx b/src/components/blocks/editor/VirtualMachineBlockEditor.tsx index c7e840a..fa46af6 100644 --- a/src/components/blocks/editor/VirtualMachineBlockEditor.tsx +++ b/src/components/blocks/editor/VirtualMachineBlockEditor.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { VirtualMachineBlockNodeProps } from '../VirtualMachineBlockNode.tsx'; import { CommonBlockNodeContentProps } from './BlockNodeEditDrawer.tsx'; +import { validateInput, validationConfig } from '../../../utils/validators.ts'; const { Option } = Select; @@ -34,7 +35,11 @@ export function VirtualMachineBlockEditor({ label="리소스 이름" name={'blockTitle'} - rules={[{ required: true, message: '리소스 이름은 필수 필드입니다!' }]} + rules={[{ + validator(_, value) { + return validateInput({ value, ...validationConfig.blockTitle }) + } + }]} initialValue={node.data.blockTitle} > @@ -44,7 +49,11 @@ export function VirtualMachineBlockEditor({ label="리소스 설명" name={'blockDescription'} rules={[ - { required: true, message: '리소스에 대한 설명은 필수 입니다!' }, + { + validator(_, value) { + return validateInput({ value, ...validationConfig.blockDescription }) + } + } ]} initialValue={node.data.blockDescription} > diff --git a/src/components/blocks/editor/WebServerBlockEditor.tsx b/src/components/blocks/editor/WebServerBlockEditor.tsx index 3d2b20e..3e107b5 100644 --- a/src/components/blocks/editor/WebServerBlockEditor.tsx +++ b/src/components/blocks/editor/WebServerBlockEditor.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { WebServerBlockNodeProps } from '../WebServerBlockNode.tsx'; import { CommonBlockNodeContentProps } from './BlockNodeEditDrawer.tsx'; +import { validateInput, validationConfig } from '../../../utils/validators.ts'; const { Option } = Select; @@ -33,7 +34,11 @@ export function WebServerBlockEditor({ label="리소스 이름" name={'blockTitle'} - rules={[{ required: true, message: '리소스 이름은 필수 필드입니다!' }]} + rules={[{ + validator(_, value) { + return validateInput({ value, ...validationConfig.blockTitle }); + } + }]} initialValue={node.data.blockTitle} > @@ -43,7 +48,12 @@ export function WebServerBlockEditor({ label="리소스 설명" name={'blockDescription'} rules={[ - { required: true, message: '리소스에 대한 설명은 필수 입니다!' }, + { + validator(_, value) { + return validateInput({ value, ...validationConfig.blockDescription }); + + } + }, ]} initialValue={node.data.blockDescription} > diff --git a/src/components/preferences/ChannelMemberPreferences.tsx b/src/components/preferences/ChannelMemberPreferences.tsx index fc6c446..126198b 100644 --- a/src/components/preferences/ChannelMemberPreferences.tsx +++ b/src/components/preferences/ChannelMemberPreferences.tsx @@ -32,6 +32,8 @@ import { } from '../../libs/core-api/api'; import { CustomModal } from '../CustomModal.tsx'; import { useUserContext } from '../providers/UserContextProvider.tsx'; +import { InputRuled } from '../InputRuled.tsx'; +import { validationConfig } from '../../utils/validators.ts'; type ChannelMemberData = { userId: string; @@ -206,16 +208,17 @@ export function ChannelMemberPreferences({ channelId }: { channelId: string }) { }} > - setSearchParams(value)} size={'large'} - onInput={(e) => setSearchParams(e.currentTarget.value)} style={{ border: 'none', borderRadius: '0', borderBottom: '1px solid #d9d9d9', }} - /> + {...validationConfig.searchInput} + /> handleInstall()} - onCancel={() => setModalVisible(false)} + onCancel={() => + setModalVisible(false)} >
{plugin.pluginTypeDefinitions.map((data) => ( @@ -145,6 +147,16 @@ function PluginInstallModal({ key={`${plugin.id}.${data.fieldName}`} label={data.fieldName} name={data.fieldName ?? ''} + rules={[{ + validator(_, value) { + const validationConfigName = plugin.id + "-" + data.fieldName; + + return validateInput({ + value, + ...validationConfig[validationConfigName as keyof typeof validationConfig], + }); + } + }]} > {data.isSecret ? ( diff --git a/src/components/preferences/GeneralAccountPreferences.tsx b/src/components/preferences/GeneralAccountPreferences.tsx index e3bcc43..69a45eb 100644 --- a/src/components/preferences/GeneralAccountPreferences.tsx +++ b/src/components/preferences/GeneralAccountPreferences.tsx @@ -11,6 +11,8 @@ import { UserContext } from '../../types/UserContext.ts'; import { LocalStorageUtils } from '../../utils/LocalStorageUtils.ts'; import { ImageUploader } from '../ProfileImageUploader.tsx'; import { useUserContext } from '../providers/UserContextProvider.tsx'; +import { InputRuled } from '../InputRuled.tsx'; +import { validationConfig } from '../../utils/validators.ts'; export function GeneralAccountPreferences({ setCurrent, @@ -77,14 +79,15 @@ export function GeneralAccountPreferences({ />
선호하는 이름 - { + { preferenceEditCallback( userContext, - (event.target as HTMLInputElement).value, + inputValue, ); }} + {...validationConfig.userName} />
diff --git a/src/components/preferences/GeneralChannelPreferences.tsx b/src/components/preferences/GeneralChannelPreferences.tsx index dc6a27f..dbe1e9b 100644 --- a/src/components/preferences/GeneralChannelPreferences.tsx +++ b/src/components/preferences/GeneralChannelPreferences.tsx @@ -5,6 +5,8 @@ import { useCallback, useEffect, useState } from 'react'; import { channelApi } from '../../api'; import { useChannelInformationQuery } from '../../api/queries.tsx'; import { ImageUploader } from '../ProfileImageUploader.tsx'; +import { InputRuled } from '../InputRuled.tsx'; +import { validationConfig } from '../../utils/validators.ts'; export function GeneralChannelPreferences({ channelId, @@ -90,34 +92,36 @@ export function GeneralChannelPreferences({
채널 이름 - { - setChannelName((event.target as HTMLInputElement).value); + callback={(inputValue:string) => { + setChannelName(inputValue); channelPreferenceEditCallback( channelId, channelDescription, - (event.target as HTMLInputElement).value, + inputValue, ); }} + {...validationConfig.channelName} />
채널 설명 - { + callback={(inputValue) => { setChannelDescription( - (event.target as HTMLInputElement).value, + inputValue ); channelPreferenceEditCallback( channelId, - (event.target as HTMLInputElement).value, + inputValue, channelName, ); }} + {...validationConfig.channelDescription} />
diff --git a/src/types/BlockTypes.ts b/src/types/BlockTypes.ts index 07ac009..a1548dc 100644 --- a/src/types/BlockTypes.ts +++ b/src/types/BlockTypes.ts @@ -28,7 +28,7 @@ export type WebServerBlock = Block & { imageTags: string; username: string | undefined; secrets: string | undefined; - containerPort: number; + containerPort: number|undefined; }; connectionMetadata: { dbRef: string; diff --git a/src/utils/InputUtils.ts b/src/utils/InputUtils.ts new file mode 100644 index 0000000..050c3c0 --- /dev/null +++ b/src/utils/InputUtils.ts @@ -0,0 +1,69 @@ +import { InputStatus } from "antd/es/_util/statusUtils"; + +export type InputRuledProps = { + type: string, + value?: string, + maxLength?: number, + minLength?: number, + placeholder?:string, +} + +export type ErrorType = 'length_min' | 'length_max' | 'type_err'; + +export type CheckInputType = { + status: InputStatus, + errorType?: ErrorType, + message: string, +} +// 조건에 맞는 input인지 확인하는 함수 +export const checkInput = ({ ...props }: InputRuledProps): CheckInputType => { + // 최소 길이 확인 + if (props.minLength && props.value!.length < props.minLength) { + return { status: 'error', errorType: 'length_min', message: `최소 ${props.minLength}자 이상 입력해주세요.` } + } + else if (props.maxLength && props.value!.length > props.maxLength) { + return { status: 'error', errorType: 'length_max', message: `최대 ${props.maxLength}자 이하로 입력해주세요.` }; + } + + // length 확인이 완료된 경우, type 확인 + // type 확인 + + // 모든 조건을 충족하는 경우 + return typeCheck(props.value!, props.type); +} + +const typeCheck = (value: string, type: string): CheckInputType => { + var result:CheckInputType = { status: '', message: '' }; + switch (type) { + case 'number': + if (isNaN(Number(value))) { + result = { status: 'error', message: '숫자만 입력해주세요.' }; + } + break; + case 'email': + const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + if (!emailRegex.test(value)) { + result = { status: 'error', message: '이메일 형식으로 입력해주세요.(ex : abc@kangdroid.com)' }; + } + break; + case 'string': + const stringRegex = /^[a-zA-Z가-힣0-9\s]*$/; + if (!stringRegex.test(value)) { + result = { status: 'error', message: '특수문자는 입력할 수 없습니다.' }; + } + break; + case 'english': + const englishRegex = /^[a-zA-Z]*$/; + if (!englishRegex.test(value)) { + result = { status: 'error', message: '영어만 입력해주세요.' }; + } + break; + default: + result = { status: '', message: '' }; + } + + if(result!.status === 'error') + result.errorType = 'type_err'; + + return result; +} \ No newline at end of file diff --git a/src/utils/validators.ts b/src/utils/validators.ts new file mode 100644 index 0000000..1417075 --- /dev/null +++ b/src/utils/validators.ts @@ -0,0 +1,99 @@ +import { InputRuledProps, checkInput } from "./InputUtils"; + +// Form.Item의 rules의 validator에서 input의 값을 검증 +// 값에 따라 Form 제출을 reject, resolve할 수 있음. +export const validateInput = ({ ...props }: InputRuledProps) => { + const checkResult = checkInput({ ...props }); + console.log(checkResult); + if (checkResult.status === 'error') { + return Promise.reject(checkResult.message); + } else { + return Promise.resolve(); + } +}; + +// Input 형태 정의 +export const validationConfig = { + channelName: { + maxLength: 40, + minLength: 1, + type: 'string' + }, + channelDescription: { + maxLength: 200, + minLength: 1, + type: 'string' + }, + channelProfileImg: { + type: 'image' + }, + userName: { + maxLength: 40, + minLength: 1, + type: 'string' + }, + userEmail: { + maxLength: 320, + minLength: 1, + type: 'email' + }, + userImg: { + type: 'image' + }, + sketchName: { + maxLength: 40, + minLength: 1, + type: 'string' + }, + sketchDescription: { + maxLength: 300, + minLength: 1, + type: 'string' + }, + blockTitle: { + maxLength: 20, + minLength: 1, + type: 'string' + }, + blockDescription: { + maxLength: 200, + minLength: 1, + type: 'string' + }, + "aws-static-AccessKey": { + maxLength: 40, + type: 'string' + }, + "aws-static-SecretKey": { + maxLength: 40, + type: 'string' + }, + "aws-static-Region": { + maxLength: 30, + type: 'string' + }, + "azure-static-ClientId": { + maxLength: 60, + type: 'string' + }, + "azure-static-ClientSecret": { + maxLength: 60, + type: 'string' + }, + "azure-static-SubscriptionId": { + maxLength: 60, + type: 'string' + }, + "azure-static-TenantId": { + maxLength: 60, + type: 'string' + }, + "azure-static-Region": { + maxLength: 40, + type: 'string' + }, + searchInput:{ + maxLength: 200, + type: 'string' + } +}; \ No newline at end of file