Skip to content

Commit

Permalink
feat(fe): add Zod error handling (#245)
Browse files Browse the repository at this point in the history
* refactor: move validation utilities to auth feature and enhance email validation

* refactor: enhance request and response validation using Zod schemas

* style: apply changes

* test: update login button locator and enhance mock response in HomePage tests
  • Loading branch information
cjeongmin authored Dec 3, 2024
1 parent d1f0ded commit 06caf72
Show file tree
Hide file tree
Showing 59 changed files with 519 additions and 1,146 deletions.
1 change: 1 addition & 0 deletions apps/client/.prettierrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"tabWidth": 2,
"useTabs": false,
"printWidth": 120,
"singleQuote": true,
"jsxSingleQuote": true,
"plugins": ["prettier-plugin-tailwindcss"]
Expand Down
5 changes: 1 addition & 4 deletions apps/client/src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ import { forwardRef, HTMLAttributes, PropsWithChildren } from 'react';

type ButtonProps = PropsWithChildren<HTMLAttributes<HTMLButtonElement>>;

const Button = forwardRef<
HTMLButtonElement,
ButtonProps & { disabled?: boolean }
>((props, ref) => {
const Button = forwardRef<HTMLButtonElement, ButtonProps & { disabled?: boolean }>((props, ref) => {
const { children, className, disabled, onClick } = props;

return (
Expand Down
4 changes: 1 addition & 3 deletions apps/client/src/components/FeatureCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ function FeatureCard({ icon, title, description }: FeatureCardProps) {
<div className='inline-flex shrink grow basis-0 flex-col items-start gap-4 self-stretch rounded-lg bg-gray-50 p-6'>
{icon}
<div className='text-lg font-bold text-black'>{title}</div>
<div className='self-stretch text-sm font-medium text-gray-500'>
{description}
</div>
<div className='self-stretch text-sm font-medium text-gray-500'>{description}</div>
</div>
);
}
Expand Down
23 changes: 5 additions & 18 deletions apps/client/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,9 @@ function Header() {

const addToast = useToastStore((state) => state.addToast);

const { Modal: SignUp, openModal: openSignUpModal } = useModal(
<SignUpModal />,
);
const { Modal: SignUp, openModal: openSignUpModal } = useModal(<SignUpModal />);

const { Modal: SignIn, openModal: openSignInModal } = useModal(
<SignInModal />,
);
const { Modal: SignIn, openModal: openSignInModal } = useModal(<SignInModal />);

const navigate = useNavigate();

Expand Down Expand Up @@ -47,19 +43,10 @@ function Header() {
className='hover:bg-gray-200 hover:text-white hover:transition-all'
onClick={isLogin() ? handleLogout : openSignInModal}
>
<p className='text-base font-bold text-black'>
{isLogin() ? '로그아웃' : '로그인'}
</p>
<p className='text-base font-bold text-black'>{isLogin() ? '로그아웃' : '로그인'}</p>
</Button>
<Button
className='bg-indigo-600'
onClick={
isLogin() ? () => navigate({ to: '/my' }) : openSignUpModal
}
>
<p className='text-base font-bold text-white'>
{isLogin() ? '세션 기록' : '회원가입'}
</p>
<Button className='bg-indigo-600' onClick={isLogin() ? () => navigate({ to: '/my' }) : openSignUpModal}>
<p className='text-base font-bold text-white'>{isLogin() ? '세션 기록' : '회원가입'}</p>
</Button>
</div>
</div>
Expand Down
93 changes: 36 additions & 57 deletions apps/client/src/components/modal/CreateQuestionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ import Markdown from 'react-markdown';

import { useModalContext } from '@/features/modal';
import { useSessionStore } from '@/features/session';
import {
patchQuestionBody,
postQuestion,
Question,
} from '@/features/session/qna';
import { patchQuestionBody, postQuestion, Question } from '@/features/session/qna';
import { useToastStore } from '@/features/toast';

import { Button } from '@/components';
Expand All @@ -22,58 +18,45 @@ function CreateQuestionModal({ question }: CreateQuestionModalProps) {

const { closeModal } = useModalContext();

const { sessionId, sessionToken, expired, addQuestion, updateQuestion } =
useSessionStore();
const { sessionId, sessionToken, expired, addQuestion, updateQuestion } = useSessionStore();

const [body, setBody] = useState('');

const { mutate: postQuestionQuery, isPending: isPostInProgress } =
useMutation({
mutationFn: postQuestion,
onSuccess: (response) => {
addQuestion(response.question);
addToast({
type: 'SUCCESS',
message: '질문이 성공적으로 등록되었습니다.',
duration: 3000,
});
closeModal();
},
onError: console.error,
});
const { mutate: postQuestionQuery, isPending: isPostInProgress } = useMutation({
mutationFn: postQuestion,
onSuccess: (response) => {
addQuestion(response.question);
addToast({
type: 'SUCCESS',
message: '질문이 성공적으로 등록되었습니다.',
duration: 3000,
});
closeModal();
},
onError: console.error,
});

const { mutate: patchQuestionBodyQuery, isPending: isPatchInProgress } =
useMutation({
mutationFn: (params: {
questionId: number;
token: string;
sessionId: string;
body: string;
}) =>
patchQuestionBody(params.questionId, {
token: params.token,
sessionId: params.sessionId,
body: params.body,
}),
onSuccess: (response) => {
updateQuestion(response.question);
addToast({
type: 'SUCCESS',
message: '질문이 성공적으로 수정되었습니다.',
duration: 3000,
});
closeModal();
},
onError: console.error,
});
const { mutate: patchQuestionBodyQuery, isPending: isPatchInProgress } = useMutation({
mutationFn: (params: { questionId: number; token: string; sessionId: string; body: string }) =>
patchQuestionBody(params.questionId, {
token: params.token,
sessionId: params.sessionId,
body: params.body,
}),
onSuccess: (response) => {
updateQuestion(response.question);
addToast({
type: 'SUCCESS',
message: '질문이 성공적으로 수정되었습니다.',
duration: 3000,
});
closeModal();
},
onError: console.error,
});

const submitDisabled =
expired ||
body.trim().length === 0 ||
!sessionId ||
!sessionToken ||
isPostInProgress ||
isPatchInProgress;
expired || body.trim().length === 0 || !sessionId || !sessionToken || isPostInProgress || isPatchInProgress;

const handleSubmit = () => {
if (submitDisabled) return;
Expand Down Expand Up @@ -113,9 +96,7 @@ function CreateQuestionModal({ question }: CreateQuestionModalProps) {
/>
<div className='inline-flex shrink grow basis-0 flex-col items-start justify-start gap-2 self-stretch overflow-y-auto rounded border border-gray-200 bg-white p-4'>
<Markdown className='prose prose-stone flex w-full flex-col gap-3 prose-img:rounded-md'>
{body.length === 0
? `**질문을 남겨주세요**\n\n**(마크다운 지원)**`
: body}
{body.length === 0 ? `**질문을 남겨주세요**\n\n**(마크다운 지원)**` : body}
</Markdown>
</div>
</div>
Expand All @@ -128,9 +109,7 @@ function CreateQuestionModal({ question }: CreateQuestionModalProps) {
className={`${!submitDisabled ? 'bg-indigo-600' : 'cursor-not-allowed bg-indigo-300'}`}
onClick={handleSubmit}
>
<div className='text-sm font-bold text-white'>
{question ? '수정하기' : '생성하기'}
</div>
<div className='text-sm font-bold text-white'>{question ? '수정하기' : '생성하기'}</div>
</Button>
</div>
</div>
Expand Down
71 changes: 25 additions & 46 deletions apps/client/src/components/modal/CreateReplyModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@ import Markdown from 'react-markdown';

import { useModalContext } from '@/features/modal';
import { useSessionStore } from '@/features/session';
import {
patchReplyBody,
postReply,
Question,
Reply,
} from '@/features/session/qna';
import { patchReplyBody, postReply, Question, Reply } from '@/features/session/qna';
import { useToastStore } from '@/features/toast';

import Button from '@/components/Button';
Expand All @@ -24,8 +19,7 @@ function CreateReplyModal({ question, reply }: CreateReplyModalProps) {

const { addToast } = useToastStore();

const { sessionToken, sessionId, expired, addReply, updateReply } =
useSessionStore();
const { sessionToken, sessionId, expired, addReply, updateReply } = useSessionStore();

const [body, setBody] = useState('');

Expand All @@ -49,40 +43,29 @@ function CreateReplyModal({ question, reply }: CreateReplyModalProps) {
onError: console.error,
});

const { mutate: patchReplyBodyQuery, isPending: isPatchInProgress } =
useMutation({
mutationFn: (params: {
replyId: number;
token: string;
sessionId: string;
body: string;
}) =>
patchReplyBody(params.replyId, {
token: params.token,
sessionId: params.sessionId,
body: params.body,
}),
onSuccess: (res) => {
if (reply && question) {
updateReply(question.questionId, res.reply);
addToast({
type: 'SUCCESS',
message: '답변이 성공적으로 수정되었습니다.',
duration: 3000,
});
closeModal();
}
},
onError: console.error,
});
const { mutate: patchReplyBodyQuery, isPending: isPatchInProgress } = useMutation({
mutationFn: (params: { replyId: number; token: string; sessionId: string; body: string }) =>
patchReplyBody(params.replyId, {
token: params.token,
sessionId: params.sessionId,
body: params.body,
}),
onSuccess: (res) => {
if (reply && question) {
updateReply(question.questionId, res.reply);
addToast({
type: 'SUCCESS',
message: '답변이 성공적으로 수정되었습니다.',
duration: 3000,
});
closeModal();
}
},
onError: console.error,
});

const submitDisabled =
expired ||
body.trim().length === 0 ||
!sessionId ||
!sessionToken ||
isPostInProgress ||
isPatchInProgress;
expired || body.trim().length === 0 || !sessionId || !sessionToken || isPostInProgress || isPatchInProgress;

const handleSubmit = () => {
if (submitDisabled) return;
Expand Down Expand Up @@ -130,9 +113,7 @@ function CreateReplyModal({ question, reply }: CreateReplyModalProps) {
/>
<div className='inline-flex h-full shrink grow basis-0 flex-col items-start justify-start gap-2 self-stretch overflow-y-auto rounded border border-gray-200 bg-white p-4'>
<Markdown className='prose prose-stone flex w-full flex-col gap-3 prose-img:rounded-md'>
{body.length === 0
? `**답변을 남겨주세요**\n\n**(마크다운 지원)**`
: body}
{body.length === 0 ? `**답변을 남겨주세요**\n\n**(마크다운 지원)**` : body}
</Markdown>
</div>
</div>
Expand All @@ -145,9 +126,7 @@ function CreateReplyModal({ question, reply }: CreateReplyModalProps) {
className={`${!submitDisabled ? 'bg-indigo-600' : 'cursor-not-allowed bg-indigo-300'}`}
onClick={handleSubmit}
>
<div className='text-sm font-bold text-white'>
{reply ? '수정하기' : '생성하기'}
</div>
<div className='text-sm font-bold text-white'>{reply ? '수정하기' : '생성하기'}</div>
</Button>
</div>
</div>
Expand Down
20 changes: 5 additions & 15 deletions apps/client/src/components/modal/CreateSessionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ function CreateSessionModal() {
onError: console.error,
});

const enableCreateSession =
sessionName.trim().length >= 3 && sessionName.trim().length <= 20;
const enableCreateSession = sessionName.trim().length >= 3 && sessionName.trim().length <= 20;

const handleCreateSession = () => {
if (!enableCreateSession || isPending) return;
Expand All @@ -57,30 +56,21 @@ function CreateSessionModal() {
value={sessionName}
onChange={setSessionName}
validationStatus={{
status:
sessionName.trim().length === 0 || enableCreateSession
? 'INITIAL'
: 'INVALID',
message: enableCreateSession
? '세션 이름을 입력해주세요'
: '세션 이름은 3자 이상 20자 이하로 입력해주세요',
status: sessionName.trim().length === 0 || enableCreateSession ? 'INITIAL' : 'INVALID',
message: enableCreateSession ? '세션 이름을 입력해주세요' : '세션 이름은 3자 이상 20자 이하로 입력해주세요',
}}
placeholder='세션 이름을 입력해주세요'
/>
</div>
<div className='mt-4 inline-flex items-start justify-start gap-2.5'>
<Button className='bg-gray-500' onClick={closeModal}>
<div className='w-[150px] text-sm font-medium text-white'>
취소하기
</div>
<div className='w-[150px] text-sm font-medium text-white'>취소하기</div>
</Button>
<Button
className={`transition-colors duration-200 ${enableCreateSession ? 'bg-indigo-600' : 'cursor-not-allowed bg-indigo-300'}`}
onClick={handleCreateSession}
>
<div className='w-[150px] text-sm font-medium text-white'>
세션 생성하기
</div>
<div className='w-[150px] text-sm font-medium text-white'>세션 생성하기</div>
</Button>
</div>
</Modal>
Expand Down
8 changes: 2 additions & 6 deletions apps/client/src/components/modal/DeleteConfirmModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@ function DeleteConfirmModal({ onCancel, onConfirm }: DeleteConfirmModalProps) {
closeModal();
}}
>
<span className='flex-grow text-sm font-medium text-white'>
취소하기
</span>
<span className='flex-grow text-sm font-medium text-white'>취소하기</span>
</Button>
<Button
className='w-full bg-indigo-600 transition-colors duration-200'
Expand All @@ -35,9 +33,7 @@ function DeleteConfirmModal({ onCancel, onConfirm }: DeleteConfirmModalProps) {
closeModal();
}}
>
<span className='flex-grow text-sm font-medium text-white'>
삭제하기
</span>
<span className='flex-grow text-sm font-medium text-white'>삭제하기</span>
</Button>
</div>
</div>
Expand Down
10 changes: 1 addition & 9 deletions apps/client/src/components/modal/InputField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,7 @@ const validationStyle: Record<ValidationStatus, string> = {
INVALID: 'max-h-10 text-rose-500 opacity-100',
};

function InputField({
label,
type,
value,
onKeyDown,
onChange,
placeholder,
validationStatus,
}: InputFieldProps) {
function InputField({ label, type, value, onKeyDown, onChange, placeholder, validationStatus }: InputFieldProps) {
return (
<div className='flex w-full flex-col items-center'>
<div className='gap-4r flex w-full flex-row items-center justify-start'>
Expand Down
Loading

0 comments on commit 06caf72

Please sign in to comment.