(null);
const isInternalUpdate = useRef(false);
const normalizedValue = normalizeContent(value);
+ const currentVisibleTextLength = useMemo(() => {
+ return getRichContentVisibleTextLength(normalizedValue);
+ }, [normalizedValue]);
/**
* 유효한 에디터 인스턴스를 반환합니다.
@@ -705,6 +715,14 @@ function MarkdownEditor({
)}
+ {visibleTextCounter ? (
+
+ ) : null}
+
{imageInsertError && (
{imageInsertError}
diff --git a/src/components/common/ui/rich-text/markdown-editor-core.tsx b/src/components/common/ui/rich-text/markdown-editor-core.tsx
index 9cfab9468..e8064de78 100644
--- a/src/components/common/ui/rich-text/markdown-editor-core.tsx
+++ b/src/components/common/ui/rich-text/markdown-editor-core.tsx
@@ -43,6 +43,7 @@ import {
} from 'react';
import { cn } from '@/components/common/ui/(shadcn)/lib/utils';
import Button from '@/components/common/ui/button';
+import EditorVisibleTextCounter from '@/components/common/ui/editor/editor-visible-text-counter';
import {
MARKDOWN_IMAGE_DEFAULT_WIDTH,
MARKDOWN_IMAGE_MAX_WIDTH,
@@ -64,6 +65,7 @@ import {
extractImageUrls,
getFileExtension,
} from '@/lib/rich-text/markdown-utils';
+import { getRichContentVisibleTextLength } from '@/utils/markdown-content-text';
const lowlight = createLowlight(common);
lowlight.register('kotlin', kotlin);
@@ -137,6 +139,10 @@ export interface MarkdownEditorCoreProps {
value: string;
onChange: (next: string) => void;
placeholder?: string;
+ visibleTextCounter?: {
+ helperText?: string;
+ maxLength: number;
+ };
allowedImageExtensions: readonly string[];
maxImageCount: number;
maxImageFileSize: number;
@@ -162,6 +168,7 @@ function MarkdownEditorCore({
value,
onChange,
placeholder,
+ visibleTextCounter,
allowedImageExtensions,
maxImageCount,
maxImageFileSize,
@@ -177,6 +184,9 @@ function MarkdownEditorCore({
(renderCount: number) => renderCount + 1,
0,
);
+ const currentVisibleTextLength = useMemo(() => {
+ return getRichContentVisibleTextLength(value);
+ }, [value]);
const allowedExtensionsLabel = useMemo(() => {
return allowedImageExtensions.join('/');
@@ -753,6 +763,14 @@ function MarkdownEditorCore({
)}
+ {visibleTextCounter ? (
+
+ ) : null}
+
{imageInsertError && (
{imageInsertError}
diff --git a/src/features/community/model/use-community-mutation.ts b/src/features/community/model/use-community-mutation.ts
index f35423ab3..cb3b46986 100644
--- a/src/features/community/model/use-community-mutation.ts
+++ b/src/features/community/model/use-community-mutation.ts
@@ -295,8 +295,8 @@ export const useCreateCommunityPostMutation = () => {
idempotencyKey: Parameters[1];
}) => createCommunityPost(request, idempotencyKey),
onError: () => {},
- onSuccess: async () => {
- await invalidateCommunityFeedQueries(queryClient);
+ onSuccess: () => {
+ invalidateCommunityFeedQueries(queryClient).catch(() => {});
},
});
};
@@ -315,11 +315,11 @@ export const useUpdateCommunityPostMutation = () => {
request: Parameters[2];
}) => updateCommunityPost(postId, revision, request),
onError: () => {},
- onSuccess: async (_, variables) => {
- await Promise.all([
+ onSuccess: (_, variables) => {
+ Promise.all([
invalidateCommunityFeedQueries(queryClient),
invalidateCommunityPostQueries(queryClient, variables.postId),
- ]);
+ ]).catch(() => {});
},
});
};
diff --git a/src/features/community/model/use-community-qna-mutation.ts b/src/features/community/model/use-community-qna-mutation.ts
index 4395547fa..bbf7f6246 100644
--- a/src/features/community/model/use-community-qna-mutation.ts
+++ b/src/features/community/model/use-community-qna-mutation.ts
@@ -72,14 +72,14 @@ export const useCreateCommunityQnaQuestionMutation = () => {
mapCommunityQnaQuestionDetail,
),
onError: () => {},
- onSuccess: async (response) => {
- await Promise.all([
+ onSuccess: (response) => {
+ Promise.all([
invalidateCommunityQnaQuestionListQueries(queryClient),
invalidateCommunityQnaQuestionAggregateQueries(
queryClient,
response.id,
),
- ]);
+ ]).catch(() => {});
},
});
};
@@ -101,14 +101,14 @@ export const useUpdateCommunityQnaQuestionMutation = () => {
mapCommunityQnaQuestionDetail,
),
onError: () => {},
- onSuccess: async (_, variables) => {
- await Promise.all([
+ onSuccess: (_, variables) => {
+ Promise.all([
invalidateCommunityQnaQuestionListQueries(queryClient),
invalidateCommunityQnaQuestionAggregateQueries(
queryClient,
variables.questionId,
),
- ]);
+ ]).catch(() => {});
},
});
};
diff --git a/src/features/community/model/use-community-qna-question-write-controller.ts b/src/features/community/model/use-community-qna-question-write-controller.ts
index 1aa880f7c..b23d152e9 100644
--- a/src/features/community/model/use-community-qna-question-write-controller.ts
+++ b/src/features/community/model/use-community-qna-question-write-controller.ts
@@ -196,7 +196,6 @@ export const useCommunityQnaQuestionWriteController = ({
request,
});
- showToast('질문을 수정했습니다.');
router.push(buildCommunityQuestionHref(updatedQuestion.id, returnPage));
return;
@@ -207,7 +206,6 @@ export const useCommunityQnaQuestionWriteController = ({
idempotencyKey: createCommunityQnaIdempotencyKey('community-question'),
});
- showToast('질문을 등록했습니다.');
router.push(buildCommunityQuestionHref(createdQuestion.id, returnPage));
} catch (error) {
const contentErrorMessage = getCommunityWriteContentErrorMessage(error);
diff --git a/src/features/community/model/use-community-write-controller.ts b/src/features/community/model/use-community-write-controller.ts
index 43370a1ad..87c87c81a 100644
--- a/src/features/community/model/use-community-write-controller.ts
+++ b/src/features/community/model/use-community-write-controller.ts
@@ -232,7 +232,6 @@ export const useCommunityWriteController = ({
},
});
- showToast('글을 수정했습니다.');
router.push(buildCommunityPostHref(updatedPost.postId, returnPage));
return;
@@ -248,7 +247,6 @@ export const useCommunityWriteController = ({
createCommunityQnaIdempotencyKey('community-question'),
});
- showToast('질문을 등록했습니다.');
router.push(buildCommunityQuestionHref(createdQuestion.id, returnPage));
return;
@@ -263,7 +261,6 @@ export const useCommunityWriteController = ({
idempotencyKey: createCommunityIdempotencyKey('community-post'),
});
- showToast('글을 등록했습니다.');
router.push(buildCommunityPostHref(createdPost.postId, returnPage));
} catch (error) {
const contentErrorMessage = getCommunityWriteContentErrorMessage(error);
diff --git a/src/features/community/ui/community-markdown-editor.tsx b/src/features/community/ui/community-markdown-editor.tsx
index a2ca45724..d3de96daa 100644
--- a/src/features/community/ui/community-markdown-editor.tsx
+++ b/src/features/community/ui/community-markdown-editor.tsx
@@ -17,6 +17,10 @@ interface CommunityMarkdownEditorProps {
value: string;
onChange: (next: string) => void;
placeholder?: string;
+ visibleTextCounter?: {
+ helperText?: string;
+ maxLength: number;
+ };
requestImageUploadTicket?: MarkdownEditorCoreProps['requestImageUploadTicket'];
uploadImageFile?: MarkdownEditorCoreProps['uploadImageFile'];
}
@@ -25,6 +29,7 @@ export default function CommunityMarkdownEditor({
value,
onChange,
placeholder,
+ visibleTextCounter,
requestImageUploadTicket = requestCommunityMarkdownImageUploadTicket,
uploadImageFile = uploadCommunityMarkdownImageFile,
}: CommunityMarkdownEditorProps) {
@@ -33,6 +38,7 @@ export default function CommunityMarkdownEditor({
value={value}
onChange={onChange}
placeholder={placeholder ?? '글 내용을 작성해 주세요.'}
+ visibleTextCounter={visibleTextCounter}
allowedImageExtensions={COMMUNITY_MARKDOWN_ALLOWED_IMAGE_EXTENSIONS}
maxImageCount={COMMUNITY_MARKDOWN_MAX_IMAGE_COUNT}
maxImageFileSize={COMMUNITY_MARKDOWN_MAX_IMAGE_FILE_SIZE}
diff --git a/src/features/community/ui/community-meta-badge.test.ts b/src/features/community/ui/community-meta-badge.test.ts
new file mode 100644
index 000000000..b0636a1b3
--- /dev/null
+++ b/src/features/community/ui/community-meta-badge.test.ts
@@ -0,0 +1,30 @@
+import { describe, expect, it } from 'vitest';
+import { COMMUNITY_MEMBER_ROLE } from '@/types/community/domain';
+import { getCommunityRoleMeta } from './community-meta-badge';
+
+describe('getCommunityRoleMeta', () => {
+ it('keeps developer members on the developer badge', () => {
+ expect(getCommunityRoleMeta(COMMUNITY_MEMBER_ROLE.DEVELOPER)).toEqual({
+ color: 'blue',
+ label: '개발자',
+ });
+ });
+
+ it('shows mentors with the same badge as developers', () => {
+ expect(getCommunityRoleMeta(COMMUNITY_MEMBER_ROLE.MENTOR)).toEqual({
+ color: 'blue',
+ label: '개발자',
+ });
+ });
+
+ it('treats non-developer roles as newcomer badges', () => {
+ expect(getCommunityRoleMeta(COMMUNITY_MEMBER_ROLE.NEWCOMER)).toEqual({
+ color: 'gray',
+ label: 'IT문자',
+ });
+ expect(getCommunityRoleMeta(COMMUNITY_MEMBER_ROLE.UNKNOWN)).toEqual({
+ color: 'gray',
+ label: 'IT문자',
+ });
+ });
+});
diff --git a/src/features/community/ui/community-meta-badge.tsx b/src/features/community/ui/community-meta-badge.tsx
index 2db0362f8..c7e7e433e 100644
--- a/src/features/community/ui/community-meta-badge.tsx
+++ b/src/features/community/ui/community-meta-badge.tsx
@@ -48,8 +48,9 @@ const BOARD_META: Record<
export const getCommunityBoardMeta = (board: CommunityPostBoard) =>
BOARD_META[board];
-const ROLE_META: Record<
- CommunityMemberRole,
+const COMMUNITY_ROLE_BADGE_META: Record<
+ | typeof COMMUNITY_MEMBER_ROLE.NEWCOMER
+ | typeof COMMUNITY_MEMBER_ROLE.DEVELOPER,
{ color: BadgeColor; label: string }
> = {
[COMMUNITY_MEMBER_ROLE.NEWCOMER]: {
@@ -60,18 +61,17 @@ const ROLE_META: Record<
color: 'blue',
label: '개발자',
},
- [COMMUNITY_MEMBER_ROLE.MENTOR]: {
- color: 'orange',
- label: '멘토',
- },
- [COMMUNITY_MEMBER_ROLE.UNKNOWN]: {
- color: 'gray',
- label: '사용자',
- },
} as const;
+const COMMUNITY_DEVELOPER_ROLE_SET = new Set([
+ COMMUNITY_MEMBER_ROLE.DEVELOPER,
+ COMMUNITY_MEMBER_ROLE.MENTOR,
+]);
+
export const getCommunityRoleMeta = (role: CommunityMemberRole) =>
- ROLE_META[role];
+ COMMUNITY_DEVELOPER_ROLE_SET.has(role)
+ ? COMMUNITY_ROLE_BADGE_META[COMMUNITY_MEMBER_ROLE.DEVELOPER]
+ : COMMUNITY_ROLE_BADGE_META[COMMUNITY_MEMBER_ROLE.NEWCOMER];
export function CommunityBoardBadge({
board,
diff --git a/src/features/community/ui/community-qna-answer-compose-section.tsx b/src/features/community/ui/community-qna-answer-compose-section.tsx
index 04a6932e4..6f107d502 100644
--- a/src/features/community/ui/community-qna-answer-compose-section.tsx
+++ b/src/features/community/ui/community-qna-answer-compose-section.tsx
@@ -10,6 +10,7 @@ import type {
CommunityQnaAnswerItem,
CommunityQnaQuestionViewer,
} from '@/types/community/qna-domain';
+import { COMMUNITY_QNA_ANSWER_CONTENT_MAX_VISIBLE_LENGTH } from '@/types/schemas/community-qna-answer-write-schema';
import CommunityMarkdownEditor from './community-markdown-editor';
import CommunitySectionShell from './community-section-shell';
@@ -77,6 +78,9 @@ export default function CommunityQnaAnswerComposeSection({
value={field.value}
onChange={field.onChange}
placeholder="답변 내용을 작성해 주세요."
+ visibleTextCounter={{
+ maxLength: COMMUNITY_QNA_ANSWER_CONTENT_MAX_VISIBLE_LENGTH,
+ }}
/>
)}
/>
diff --git a/src/features/community/ui/pages/community-qna-question-write-page-client.tsx b/src/features/community/ui/pages/community-qna-question-write-page-client.tsx
index 788558e9a..637f315c1 100644
--- a/src/features/community/ui/pages/community-qna-question-write-page-client.tsx
+++ b/src/features/community/ui/pages/community-qna-question-write-page-client.tsx
@@ -12,7 +12,10 @@ import {
useCommunityQnaQuestionWriteController,
type CommunityQnaQuestionWriteMode,
} from '@/features/community/model/use-community-qna-question-write-controller';
-import { COMMUNITY_WRITE_TITLE_MAX_LENGTH } from '@/types/schemas/community-write-schema';
+import {
+ COMMUNITY_WRITE_CONTENT_MAX_VISIBLE_LENGTH,
+ COMMUNITY_WRITE_TITLE_MAX_LENGTH,
+} from '@/types/schemas/community-write-schema';
import CommunityMarkdownEditor from '../community-markdown-editor';
import CommunitySectionShell from '../community-section-shell';
@@ -131,6 +134,9 @@ export default function CommunityQnaQuestionWritePageClient({
value={field.value}
onChange={field.onChange}
placeholder="질문 내용을 자세히 작성해 주세요."
+ visibleTextCounter={{
+ maxLength: COMMUNITY_WRITE_CONTENT_MAX_VISIBLE_LENGTH,
+ }}
/>
)}
/>
diff --git a/src/features/community/ui/pages/community-write-page-client.tsx b/src/features/community/ui/pages/community-write-page-client.tsx
index cde521a50..c6bef5a26 100644
--- a/src/features/community/ui/pages/community-write-page-client.tsx
+++ b/src/features/community/ui/pages/community-write-page-client.tsx
@@ -14,7 +14,10 @@ import {
type CommunityWriteMode,
} from '@/features/community/model/use-community-write-controller';
import type { CommunityBoard } from '@/types/community/domain';
-import { COMMUNITY_WRITE_TITLE_MAX_LENGTH } from '@/types/schemas/community-write-schema';
+import {
+ COMMUNITY_WRITE_CONTENT_MAX_VISIBLE_LENGTH,
+ COMMUNITY_WRITE_TITLE_MAX_LENGTH,
+} from '@/types/schemas/community-write-schema';
import CommunityMarkdownEditor from '../community-markdown-editor';
import CommunitySectionShell from '../community-section-shell';
@@ -150,6 +153,9 @@ export default function CommunityWritePageClient({
value={field.value}
onChange={field.onChange}
placeholder="글 내용을 작성해 주세요."
+ visibleTextCounter={{
+ maxLength: COMMUNITY_WRITE_CONTENT_MAX_VISIBLE_LENGTH,
+ }}
/>
)}
/>
diff --git a/src/features/mentoring/ui/registration/markdown/mentor-markdown-editor.tsx b/src/features/mentoring/ui/registration/markdown/mentor-markdown-editor.tsx
index f1fb1c021..d712a824e 100644
--- a/src/features/mentoring/ui/registration/markdown/mentor-markdown-editor.tsx
+++ b/src/features/mentoring/ui/registration/markdown/mentor-markdown-editor.tsx
@@ -17,12 +17,17 @@ interface MentorMarkdownEditorProps {
value: string;
onChange: (next: string) => void;
placeholder?: string;
+ visibleTextCounter?: {
+ helperText?: string;
+ maxLength: number;
+ };
}
function MentorMarkdownEditor({
value,
onChange,
placeholder,
+ visibleTextCounter,
}: MentorMarkdownEditorProps) {
const handleUploadImageFile = useCallback(async (file: File) => {
const { name: fileName } = file;
@@ -44,6 +49,7 @@ function MentorMarkdownEditor({
onChange={onChange}
placeholder={placeholder}
normalizeContent={normalizeMentorMarkdownContent}
+ visibleTextCounter={visibleTextCounter}
imageConfig={{
allowedImageExtensions: MENTOR_MARKDOWN_ALLOWED_IMAGE_EXTENSIONS,
maxImageCount: MENTOR_MARKDOWN_MAX_IMAGE_COUNT,
diff --git a/src/features/mentoring/ui/registration/step-content/mentor-registration-description-step.tsx b/src/features/mentoring/ui/registration/step-content/mentor-registration-description-step.tsx
index 48b02f182..0067b025a 100644
--- a/src/features/mentoring/ui/registration/step-content/mentor-registration-description-step.tsx
+++ b/src/features/mentoring/ui/registration/step-content/mentor-registration-description-step.tsx
@@ -7,6 +7,10 @@ import FormSectionCard from '@/components/common/ui/form/form-section-card';
import MentorMarkdownEditor from '@/features/mentoring/ui/registration/markdown/mentor-markdown-editor';
import FieldRequirementBadge from '@/features/mentoring/ui/registration/mentor-field-requirement-badge';
import InterviewQuestionsTextarea from '@/features/mentoring/ui/registration/mentor-interview-questions-textarea';
+import {
+ MENTOR_DESCRIPTION_MAX_LENGTH,
+ MENTOR_DESCRIPTION_MIN_LENGTH,
+} from '@/types/schemas/mentor-registration-schema';
import type { MentorRegistrationDescriptionStepProps } from './mentor-registration-step-content.types';
export default function MentorRegistrationDescriptionStep({
@@ -47,6 +51,10 @@ export default function MentorRegistrationDescriptionStep({
value={mentorDescriptionField.value ?? ''}
onChange={mentorDescriptionField.onChange}
placeholder="멘토 소개, 전문 분야, 상담 범위를 자유롭게 작성해주세요."
+ visibleTextCounter={{
+ helperText: `최소 ${MENTOR_DESCRIPTION_MIN_LENGTH.toLocaleString()}자`,
+ maxLength: MENTOR_DESCRIPTION_MAX_LENGTH,
+ }}
/>
diff --git a/src/types/schemas/community-qna-answer-write-schema.ts b/src/types/schemas/community-qna-answer-write-schema.ts
index 23a24face..4e5b83001 100644
--- a/src/types/schemas/community-qna-answer-write-schema.ts
+++ b/src/types/schemas/community-qna-answer-write-schema.ts
@@ -1,60 +1,19 @@
import { z } from 'zod';
-import {
- COMMUNITY_MARKDOWN_ALLOWED_IMAGE_EXTENSIONS_LABEL,
- COMMUNITY_MARKDOWN_MAX_IMAGE_COUNT,
- extractImageUrls,
- hasAllowedCommunityMarkdownImageExtension,
- hasMeaningfulCommunityMarkdownContent,
- hasUnsafeCommunityMarkdownHtml,
- isCommunityMarkdownImageUrl,
-} from '@/types/community/markdown';
+import { validateCommunityWriteContent } from './community-write-schema';
+
+export const COMMUNITY_QNA_ANSWER_CONTENT_MAX_VISIBLE_LENGTH = 3_000;
export const communityQnaAnswerWriteSchema = z
.object({
content: z.string(),
})
.superRefine((values, ctx) => {
- if (hasUnsafeCommunityMarkdownHtml(values.content)) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- path: ['content'],
- message: '허용되지 않는 HTML 또는 스크립트는 사용할 수 없습니다.',
- });
- }
-
- if (!hasMeaningfulCommunityMarkdownContent(values.content)) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- path: ['content'],
- message: '본문을 입력해주세요.',
- });
- }
-
- const imageUrls = extractImageUrls(values.content);
-
- if (imageUrls.length > COMMUNITY_MARKDOWN_MAX_IMAGE_COUNT) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- path: ['content'],
- message: `이미지는 최대 ${COMMUNITY_MARKDOWN_MAX_IMAGE_COUNT}개까지 첨부할 수 있습니다.`,
- });
- }
-
- if (!imageUrls.every(isCommunityMarkdownImageUrl)) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- path: ['content'],
- message: '이미지 주소는 업로드된 안전한 URL만 사용할 수 있습니다.',
- });
- }
-
- if (!imageUrls.every(hasAllowedCommunityMarkdownImageExtension)) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- path: ['content'],
- message: `이미지는 ${COMMUNITY_MARKDOWN_ALLOWED_IMAGE_EXTENSIONS_LABEL} 형식만 사용할 수 있습니다.`,
- });
- }
+ validateCommunityWriteContent({
+ content: values.content,
+ ctx,
+ maxVisibleTextLength: COMMUNITY_QNA_ANSWER_CONTENT_MAX_VISIBLE_LENGTH,
+ maxVisibleTextMessage: `답변 본문은 보이는 글자수 기준 ${COMMUNITY_QNA_ANSWER_CONTENT_MAX_VISIBLE_LENGTH.toLocaleString()}자 이하여야 합니다.`,
+ });
});
export type CommunityQnaAnswerWriteFormValues = z.infer<
diff --git a/src/types/schemas/community-qna-question-write-schema.ts b/src/types/schemas/community-qna-question-write-schema.ts
index de19c6e46..6a3ad7c1a 100644
--- a/src/types/schemas/community-qna-question-write-schema.ts
+++ b/src/types/schemas/community-qna-question-write-schema.ts
@@ -1,14 +1,9 @@
import { z } from 'zod';
import {
- COMMUNITY_MARKDOWN_ALLOWED_IMAGE_EXTENSIONS_LABEL,
- COMMUNITY_MARKDOWN_MAX_IMAGE_COUNT,
- extractImageUrls,
- hasAllowedCommunityMarkdownImageExtension,
- hasMeaningfulCommunityMarkdownContent,
- hasUnsafeCommunityMarkdownHtml,
- isCommunityMarkdownImageUrl,
-} from '@/types/community/markdown';
-import { COMMUNITY_WRITE_TITLE_MAX_LENGTH } from './community-write-schema';
+ COMMUNITY_WRITE_CONTENT_MAX_VISIBLE_LENGTH,
+ COMMUNITY_WRITE_TITLE_MAX_LENGTH,
+ validateCommunityWriteContent,
+} from './community-write-schema';
export const communityQnaQuestionWriteSchema = z
.object({
@@ -23,47 +18,12 @@ export const communityQnaQuestionWriteSchema = z
content: z.string(),
})
.superRefine((values, ctx) => {
- if (hasUnsafeCommunityMarkdownHtml(values.content)) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- path: ['content'],
- message: '허용되지 않는 HTML 또는 스크립트는 사용할 수 없습니다.',
- });
- }
-
- if (!hasMeaningfulCommunityMarkdownContent(values.content)) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- path: ['content'],
- message: '본문을 입력해주세요.',
- });
- }
-
- const imageUrls = extractImageUrls(values.content);
-
- if (imageUrls.length > COMMUNITY_MARKDOWN_MAX_IMAGE_COUNT) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- path: ['content'],
- message: `이미지는 최대 ${COMMUNITY_MARKDOWN_MAX_IMAGE_COUNT}개까지 첨부할 수 있습니다.`,
- });
- }
-
- if (!imageUrls.every(isCommunityMarkdownImageUrl)) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- path: ['content'],
- message: '이미지 주소는 업로드된 안전한 URL만 사용할 수 있습니다.',
- });
- }
-
- if (!imageUrls.every(hasAllowedCommunityMarkdownImageExtension)) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- path: ['content'],
- message: `이미지는 ${COMMUNITY_MARKDOWN_ALLOWED_IMAGE_EXTENSIONS_LABEL} 형식만 사용할 수 있습니다.`,
- });
- }
+ validateCommunityWriteContent({
+ content: values.content,
+ ctx,
+ maxVisibleTextLength: COMMUNITY_WRITE_CONTENT_MAX_VISIBLE_LENGTH,
+ maxVisibleTextMessage: `본문은 보이는 글자수 기준 ${COMMUNITY_WRITE_CONTENT_MAX_VISIBLE_LENGTH.toLocaleString()}자 이하여야 합니다.`,
+ });
});
export type CommunityQnaQuestionWriteFormValues = z.infer<
diff --git a/src/types/schemas/community-write-schema.ts b/src/types/schemas/community-write-schema.ts
index fc4988631..f85e83ff4 100644
--- a/src/types/schemas/community-write-schema.ts
+++ b/src/types/schemas/community-write-schema.ts
@@ -9,8 +9,10 @@ import {
hasUnsafeCommunityMarkdownHtml,
isCommunityMarkdownImageUrl,
} from '@/types/community/markdown';
+import { getRichContentVisibleTextLength } from '@/utils/markdown-content-text';
export const COMMUNITY_WRITE_TITLE_MAX_LENGTH = 120;
+export const COMMUNITY_WRITE_CONTENT_MAX_VISIBLE_LENGTH = 5_000;
const COMMUNITY_BOARD_VALUES = [
COMMUNITY_BOARD.QNA,
@@ -19,6 +21,70 @@ const COMMUNITY_BOARD_VALUES = [
COMMUNITY_BOARD.KNOWLEDGE,
] as const;
+interface ValidateCommunityWriteContentParams {
+ content: string;
+ ctx: z.RefinementCtx;
+ maxVisibleTextLength: number;
+ maxVisibleTextMessage: string;
+}
+
+export const validateCommunityWriteContent = ({
+ content,
+ ctx,
+ maxVisibleTextLength,
+ maxVisibleTextMessage,
+}: ValidateCommunityWriteContentParams) => {
+ if (hasUnsafeCommunityMarkdownHtml(content)) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: ['content'],
+ message: '허용되지 않는 HTML 또는 스크립트는 사용할 수 없습니다.',
+ });
+ }
+
+ if (!hasMeaningfulCommunityMarkdownContent(content)) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: ['content'],
+ message: '본문을 입력해주세요.',
+ });
+ }
+
+ const imageUrls = extractImageUrls(content);
+
+ if (imageUrls.length > COMMUNITY_MARKDOWN_MAX_IMAGE_COUNT) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: ['content'],
+ message: `이미지는 최대 ${COMMUNITY_MARKDOWN_MAX_IMAGE_COUNT}개까지 첨부할 수 있습니다.`,
+ });
+ }
+
+ if (!imageUrls.every(isCommunityMarkdownImageUrl)) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: ['content'],
+ message: '이미지 주소는 업로드된 안전한 URL만 사용할 수 있습니다.',
+ });
+ }
+
+ if (!imageUrls.every(hasAllowedCommunityMarkdownImageExtension)) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: ['content'],
+ message: `이미지는 ${COMMUNITY_MARKDOWN_ALLOWED_IMAGE_EXTENSIONS_LABEL} 형식만 사용할 수 있습니다.`,
+ });
+ }
+
+ if (getRichContentVisibleTextLength(content) > maxVisibleTextLength) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: ['content'],
+ message: maxVisibleTextMessage,
+ });
+ }
+};
+
export const communityWriteSchema = z
.object({
board: z.enum(COMMUNITY_BOARD_VALUES),
@@ -33,47 +99,12 @@ export const communityWriteSchema = z
content: z.string(),
})
.superRefine((values, ctx) => {
- if (hasUnsafeCommunityMarkdownHtml(values.content)) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- path: ['content'],
- message: '허용되지 않는 HTML 또는 스크립트는 사용할 수 없습니다.',
- });
- }
-
- if (!hasMeaningfulCommunityMarkdownContent(values.content)) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- path: ['content'],
- message: '본문을 입력해주세요.',
- });
- }
-
- const imageUrls = extractImageUrls(values.content);
-
- if (imageUrls.length > COMMUNITY_MARKDOWN_MAX_IMAGE_COUNT) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- path: ['content'],
- message: `이미지는 최대 ${COMMUNITY_MARKDOWN_MAX_IMAGE_COUNT}개까지 첨부할 수 있습니다.`,
- });
- }
-
- if (!imageUrls.every(isCommunityMarkdownImageUrl)) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- path: ['content'],
- message: '이미지 주소는 업로드된 안전한 URL만 사용할 수 있습니다.',
- });
- }
-
- if (!imageUrls.every(hasAllowedCommunityMarkdownImageExtension)) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- path: ['content'],
- message: `이미지는 ${COMMUNITY_MARKDOWN_ALLOWED_IMAGE_EXTENSIONS_LABEL} 형식만 사용할 수 있습니다.`,
- });
- }
+ validateCommunityWriteContent({
+ content: values.content,
+ ctx,
+ maxVisibleTextLength: COMMUNITY_WRITE_CONTENT_MAX_VISIBLE_LENGTH,
+ maxVisibleTextMessage: `본문은 보이는 글자수 기준 ${COMMUNITY_WRITE_CONTENT_MAX_VISIBLE_LENGTH.toLocaleString()}자 이하여야 합니다.`,
+ });
});
export type CommunityWriteFormValues = z.infer;
diff --git a/src/types/schemas/mentor-registration-schema.ts b/src/types/schemas/mentor-registration-schema.ts
index 502939a11..2134b0cc0 100644
--- a/src/types/schemas/mentor-registration-schema.ts
+++ b/src/types/schemas/mentor-registration-schema.ts
@@ -18,6 +18,7 @@ import {
MENTOR_CAREER_ENTRY_MAX_COUNT,
WEEKDAY_KEYS,
} from '@/types/mentoring/settings';
+import { getRichContentVisibleTextLength } from '@/utils/markdown-content-text';
export const MENTORING_TITLE_MIN_LENGTH = 10;
export const MENTORING_TITLE_MAX_LENGTH = 40;
@@ -29,7 +30,7 @@ export const CAREER_ENTRY_MAX_COUNT = MENTOR_CAREER_ENTRY_MAX_COUNT;
export const MAJOR_HISTORY_ENTRY_MAX_LENGTH = 60;
export const SCHEDULE_DAY_MAX_SLOT_COUNT = 48;
export const MENTOR_DESCRIPTION_MIN_LENGTH = 30;
-export const MENTOR_DESCRIPTION_MAX_LENGTH = 30_000;
+export const MENTOR_DESCRIPTION_MAX_LENGTH = 5_000;
export const INTERVIEW_QUESTION_MIN_LENGTH = 8;
export const INTERVIEW_QUESTION_MAX_LENGTH = 120;
export const INTERVIEW_QUESTION_MAX_COUNT = 8;
@@ -356,14 +357,7 @@ export const mentorRegistrationSchema = z
scheduleDrafts: scheduleDraftsSchema.default(
createEmptyMentorScheduleDrafts,
),
- detailedDescription: z
- .string()
- .trim()
- .max(
- MENTOR_DESCRIPTION_MAX_LENGTH,
- `멘토 소개는 ${MENTOR_DESCRIPTION_MAX_LENGTH.toLocaleString()}자 이하로 입력해주세요.`,
- )
- .default(''),
+ detailedDescription: z.string().trim().default(''),
interviewQuestions: z
.array(
z
@@ -392,6 +386,9 @@ export const mentorRegistrationSchema = z
const normalizedDescription = normalizeMentorMarkdownContent(
values.detailedDescription,
);
+ const mentorDescriptionVisibleTextLength = getRichContentVisibleTextLength(
+ normalizedDescription,
+ );
const validateEnabledPriceRange = ({
enabled,
price,
@@ -422,12 +419,13 @@ export const mentorRegistrationSchema = z
if (
normalizedDescription.length > 0 &&
- normalizedDescription.length < MENTOR_DESCRIPTION_MIN_LENGTH
+ (mentorDescriptionVisibleTextLength < MENTOR_DESCRIPTION_MIN_LENGTH ||
+ mentorDescriptionVisibleTextLength > MENTOR_DESCRIPTION_MAX_LENGTH)
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['detailedDescription'],
- message: `멘토 소개는 ${MENTOR_DESCRIPTION_MIN_LENGTH}자 이상 입력해주세요.`,
+ message: `멘토 소개는 보이는 글자수 기준 ${MENTOR_DESCRIPTION_MIN_LENGTH.toLocaleString()}자 이상 ${MENTOR_DESCRIPTION_MAX_LENGTH.toLocaleString()}자 이하여야 합니다.`,
});
}