Skip to content

Commit 06caf72

Browse files
authored
feat(fe): add Zod error handling (#245)
* 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
1 parent d1f0ded commit 06caf72

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+519
-1146
lines changed

apps/client/.prettierrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"tabWidth": 2,
33
"useTabs": false,
4+
"printWidth": 120,
45
"singleQuote": true,
56
"jsxSingleQuote": true,
67
"plugins": ["prettier-plugin-tailwindcss"]

apps/client/src/components/Button.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@ import { forwardRef, HTMLAttributes, PropsWithChildren } from 'react';
22

33
type ButtonProps = PropsWithChildren<HTMLAttributes<HTMLButtonElement>>;
44

5-
const Button = forwardRef<
6-
HTMLButtonElement,
7-
ButtonProps & { disabled?: boolean }
8-
>((props, ref) => {
5+
const Button = forwardRef<HTMLButtonElement, ButtonProps & { disabled?: boolean }>((props, ref) => {
96
const { children, className, disabled, onClick } = props;
107

118
return (

apps/client/src/components/FeatureCard.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ function FeatureCard({ icon, title, description }: FeatureCardProps) {
99
<div className='inline-flex shrink grow basis-0 flex-col items-start gap-4 self-stretch rounded-lg bg-gray-50 p-6'>
1010
{icon}
1111
<div className='text-lg font-bold text-black'>{title}</div>
12-
<div className='self-stretch text-sm font-medium text-gray-500'>
13-
{description}
14-
</div>
12+
<div className='self-stretch text-sm font-medium text-gray-500'>{description}</div>
1513
</div>
1614
);
1715
}

apps/client/src/components/Header.tsx

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,9 @@ function Header() {
1111

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

14-
const { Modal: SignUp, openModal: openSignUpModal } = useModal(
15-
<SignUpModal />,
16-
);
14+
const { Modal: SignUp, openModal: openSignUpModal } = useModal(<SignUpModal />);
1715

18-
const { Modal: SignIn, openModal: openSignInModal } = useModal(
19-
<SignInModal />,
20-
);
16+
const { Modal: SignIn, openModal: openSignInModal } = useModal(<SignInModal />);
2117

2218
const navigate = useNavigate();
2319

@@ -47,19 +43,10 @@ function Header() {
4743
className='hover:bg-gray-200 hover:text-white hover:transition-all'
4844
onClick={isLogin() ? handleLogout : openSignInModal}
4945
>
50-
<p className='text-base font-bold text-black'>
51-
{isLogin() ? '로그아웃' : '로그인'}
52-
</p>
46+
<p className='text-base font-bold text-black'>{isLogin() ? '로그아웃' : '로그인'}</p>
5347
</Button>
54-
<Button
55-
className='bg-indigo-600'
56-
onClick={
57-
isLogin() ? () => navigate({ to: '/my' }) : openSignUpModal
58-
}
59-
>
60-
<p className='text-base font-bold text-white'>
61-
{isLogin() ? '세션 기록' : '회원가입'}
62-
</p>
48+
<Button className='bg-indigo-600' onClick={isLogin() ? () => navigate({ to: '/my' }) : openSignUpModal}>
49+
<p className='text-base font-bold text-white'>{isLogin() ? '세션 기록' : '회원가입'}</p>
6350
</Button>
6451
</div>
6552
</div>

apps/client/src/components/modal/CreateQuestionModal.tsx

Lines changed: 36 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,7 @@ import Markdown from 'react-markdown';
44

55
import { useModalContext } from '@/features/modal';
66
import { useSessionStore } from '@/features/session';
7-
import {
8-
patchQuestionBody,
9-
postQuestion,
10-
Question,
11-
} from '@/features/session/qna';
7+
import { patchQuestionBody, postQuestion, Question } from '@/features/session/qna';
128
import { useToastStore } from '@/features/toast';
139

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

2319
const { closeModal } = useModalContext();
2420

25-
const { sessionId, sessionToken, expired, addQuestion, updateQuestion } =
26-
useSessionStore();
21+
const { sessionId, sessionToken, expired, addQuestion, updateQuestion } = useSessionStore();
2722

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

30-
const { mutate: postQuestionQuery, isPending: isPostInProgress } =
31-
useMutation({
32-
mutationFn: postQuestion,
33-
onSuccess: (response) => {
34-
addQuestion(response.question);
35-
addToast({
36-
type: 'SUCCESS',
37-
message: '질문이 성공적으로 등록되었습니다.',
38-
duration: 3000,
39-
});
40-
closeModal();
41-
},
42-
onError: console.error,
43-
});
25+
const { mutate: postQuestionQuery, isPending: isPostInProgress } = useMutation({
26+
mutationFn: postQuestion,
27+
onSuccess: (response) => {
28+
addQuestion(response.question);
29+
addToast({
30+
type: 'SUCCESS',
31+
message: '질문이 성공적으로 등록되었습니다.',
32+
duration: 3000,
33+
});
34+
closeModal();
35+
},
36+
onError: console.error,
37+
});
4438

45-
const { mutate: patchQuestionBodyQuery, isPending: isPatchInProgress } =
46-
useMutation({
47-
mutationFn: (params: {
48-
questionId: number;
49-
token: string;
50-
sessionId: string;
51-
body: string;
52-
}) =>
53-
patchQuestionBody(params.questionId, {
54-
token: params.token,
55-
sessionId: params.sessionId,
56-
body: params.body,
57-
}),
58-
onSuccess: (response) => {
59-
updateQuestion(response.question);
60-
addToast({
61-
type: 'SUCCESS',
62-
message: '질문이 성공적으로 수정되었습니다.',
63-
duration: 3000,
64-
});
65-
closeModal();
66-
},
67-
onError: console.error,
68-
});
39+
const { mutate: patchQuestionBodyQuery, isPending: isPatchInProgress } = useMutation({
40+
mutationFn: (params: { questionId: number; token: string; sessionId: string; body: string }) =>
41+
patchQuestionBody(params.questionId, {
42+
token: params.token,
43+
sessionId: params.sessionId,
44+
body: params.body,
45+
}),
46+
onSuccess: (response) => {
47+
updateQuestion(response.question);
48+
addToast({
49+
type: 'SUCCESS',
50+
message: '질문이 성공적으로 수정되었습니다.',
51+
duration: 3000,
52+
});
53+
closeModal();
54+
},
55+
onError: console.error,
56+
});
6957

7058
const submitDisabled =
71-
expired ||
72-
body.trim().length === 0 ||
73-
!sessionId ||
74-
!sessionToken ||
75-
isPostInProgress ||
76-
isPatchInProgress;
59+
expired || body.trim().length === 0 || !sessionId || !sessionToken || isPostInProgress || isPatchInProgress;
7760

7861
const handleSubmit = () => {
7962
if (submitDisabled) return;
@@ -113,9 +96,7 @@ function CreateQuestionModal({ question }: CreateQuestionModalProps) {
11396
/>
11497
<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'>
11598
<Markdown className='prose prose-stone flex w-full flex-col gap-3 prose-img:rounded-md'>
116-
{body.length === 0
117-
? `**질문을 남겨주세요**\n\n**(마크다운 지원)**`
118-
: body}
99+
{body.length === 0 ? `**질문을 남겨주세요**\n\n**(마크다운 지원)**` : body}
119100
</Markdown>
120101
</div>
121102
</div>
@@ -128,9 +109,7 @@ function CreateQuestionModal({ question }: CreateQuestionModalProps) {
128109
className={`${!submitDisabled ? 'bg-indigo-600' : 'cursor-not-allowed bg-indigo-300'}`}
129110
onClick={handleSubmit}
130111
>
131-
<div className='text-sm font-bold text-white'>
132-
{question ? '수정하기' : '생성하기'}
133-
</div>
112+
<div className='text-sm font-bold text-white'>{question ? '수정하기' : '생성하기'}</div>
134113
</Button>
135114
</div>
136115
</div>

apps/client/src/components/modal/CreateReplyModal.tsx

Lines changed: 25 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,7 @@ import Markdown from 'react-markdown';
44

55
import { useModalContext } from '@/features/modal';
66
import { useSessionStore } from '@/features/session';
7-
import {
8-
patchReplyBody,
9-
postReply,
10-
Question,
11-
Reply,
12-
} from '@/features/session/qna';
7+
import { patchReplyBody, postReply, Question, Reply } from '@/features/session/qna';
138
import { useToastStore } from '@/features/toast';
149

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

2520
const { addToast } = useToastStore();
2621

27-
const { sessionToken, sessionId, expired, addReply, updateReply } =
28-
useSessionStore();
22+
const { sessionToken, sessionId, expired, addReply, updateReply } = useSessionStore();
2923

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

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

52-
const { mutate: patchReplyBodyQuery, isPending: isPatchInProgress } =
53-
useMutation({
54-
mutationFn: (params: {
55-
replyId: number;
56-
token: string;
57-
sessionId: string;
58-
body: string;
59-
}) =>
60-
patchReplyBody(params.replyId, {
61-
token: params.token,
62-
sessionId: params.sessionId,
63-
body: params.body,
64-
}),
65-
onSuccess: (res) => {
66-
if (reply && question) {
67-
updateReply(question.questionId, res.reply);
68-
addToast({
69-
type: 'SUCCESS',
70-
message: '답변이 성공적으로 수정되었습니다.',
71-
duration: 3000,
72-
});
73-
closeModal();
74-
}
75-
},
76-
onError: console.error,
77-
});
46+
const { mutate: patchReplyBodyQuery, isPending: isPatchInProgress } = useMutation({
47+
mutationFn: (params: { replyId: number; token: string; sessionId: string; body: string }) =>
48+
patchReplyBody(params.replyId, {
49+
token: params.token,
50+
sessionId: params.sessionId,
51+
body: params.body,
52+
}),
53+
onSuccess: (res) => {
54+
if (reply && question) {
55+
updateReply(question.questionId, res.reply);
56+
addToast({
57+
type: 'SUCCESS',
58+
message: '답변이 성공적으로 수정되었습니다.',
59+
duration: 3000,
60+
});
61+
closeModal();
62+
}
63+
},
64+
onError: console.error,
65+
});
7866

7967
const submitDisabled =
80-
expired ||
81-
body.trim().length === 0 ||
82-
!sessionId ||
83-
!sessionToken ||
84-
isPostInProgress ||
85-
isPatchInProgress;
68+
expired || body.trim().length === 0 || !sessionId || !sessionToken || isPostInProgress || isPatchInProgress;
8669

8770
const handleSubmit = () => {
8871
if (submitDisabled) return;
@@ -130,9 +113,7 @@ function CreateReplyModal({ question, reply }: CreateReplyModalProps) {
130113
/>
131114
<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'>
132115
<Markdown className='prose prose-stone flex w-full flex-col gap-3 prose-img:rounded-md'>
133-
{body.length === 0
134-
? `**답변을 남겨주세요**\n\n**(마크다운 지원)**`
135-
: body}
116+
{body.length === 0 ? `**답변을 남겨주세요**\n\n**(마크다운 지원)**` : body}
136117
</Markdown>
137118
</div>
138119
</div>
@@ -145,9 +126,7 @@ function CreateReplyModal({ question, reply }: CreateReplyModalProps) {
145126
className={`${!submitDisabled ? 'bg-indigo-600' : 'cursor-not-allowed bg-indigo-300'}`}
146127
onClick={handleSubmit}
147128
>
148-
<div className='text-sm font-bold text-white'>
149-
{reply ? '수정하기' : '생성하기'}
150-
</div>
129+
<div className='text-sm font-bold text-white'>{reply ? '수정하기' : '생성하기'}</div>
151130
</Button>
152131
</div>
153132
</div>

apps/client/src/components/modal/CreateSessionModal.tsx

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@ function CreateSessionModal() {
3636
onError: console.error,
3737
});
3838

39-
const enableCreateSession =
40-
sessionName.trim().length >= 3 && sessionName.trim().length <= 20;
39+
const enableCreateSession = sessionName.trim().length >= 3 && sessionName.trim().length <= 20;
4140

4241
const handleCreateSession = () => {
4342
if (!enableCreateSession || isPending) return;
@@ -57,30 +56,21 @@ function CreateSessionModal() {
5756
value={sessionName}
5857
onChange={setSessionName}
5958
validationStatus={{
60-
status:
61-
sessionName.trim().length === 0 || enableCreateSession
62-
? 'INITIAL'
63-
: 'INVALID',
64-
message: enableCreateSession
65-
? '세션 이름을 입력해주세요'
66-
: '세션 이름은 3자 이상 20자 이하로 입력해주세요',
59+
status: sessionName.trim().length === 0 || enableCreateSession ? 'INITIAL' : 'INVALID',
60+
message: enableCreateSession ? '세션 이름을 입력해주세요' : '세션 이름은 3자 이상 20자 이하로 입력해주세요',
6761
}}
6862
placeholder='세션 이름을 입력해주세요'
6963
/>
7064
</div>
7165
<div className='mt-4 inline-flex items-start justify-start gap-2.5'>
7266
<Button className='bg-gray-500' onClick={closeModal}>
73-
<div className='w-[150px] text-sm font-medium text-white'>
74-
취소하기
75-
</div>
67+
<div className='w-[150px] text-sm font-medium text-white'>취소하기</div>
7668
</Button>
7769
<Button
7870
className={`transition-colors duration-200 ${enableCreateSession ? 'bg-indigo-600' : 'cursor-not-allowed bg-indigo-300'}`}
7971
onClick={handleCreateSession}
8072
>
81-
<div className='w-[150px] text-sm font-medium text-white'>
82-
세션 생성하기
83-
</div>
73+
<div className='w-[150px] text-sm font-medium text-white'>세션 생성하기</div>
8474
</Button>
8575
</div>
8676
</Modal>

apps/client/src/components/modal/DeleteConfirmModal.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@ function DeleteConfirmModal({ onCancel, onConfirm }: DeleteConfirmModalProps) {
2424
closeModal();
2525
}}
2626
>
27-
<span className='flex-grow text-sm font-medium text-white'>
28-
취소하기
29-
</span>
27+
<span className='flex-grow text-sm font-medium text-white'>취소하기</span>
3028
</Button>
3129
<Button
3230
className='w-full bg-indigo-600 transition-colors duration-200'
@@ -35,9 +33,7 @@ function DeleteConfirmModal({ onCancel, onConfirm }: DeleteConfirmModalProps) {
3533
closeModal();
3634
}}
3735
>
38-
<span className='flex-grow text-sm font-medium text-white'>
39-
삭제하기
40-
</span>
36+
<span className='flex-grow text-sm font-medium text-white'>삭제하기</span>
4137
</Button>
4238
</div>
4339
</div>

apps/client/src/components/modal/InputField.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,7 @@ const validationStyle: Record<ValidationStatus, string> = {
1717
INVALID: 'max-h-10 text-rose-500 opacity-100',
1818
};
1919

20-
function InputField({
21-
label,
22-
type,
23-
value,
24-
onKeyDown,
25-
onChange,
26-
placeholder,
27-
validationStatus,
28-
}: InputFieldProps) {
20+
function InputField({ label, type, value, onKeyDown, onChange, placeholder, validationStatus }: InputFieldProps) {
2921
return (
3022
<div className='flex w-full flex-col items-center'>
3123
<div className='gap-4r flex w-full flex-row items-center justify-start'>

0 commit comments

Comments
 (0)