Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/components/common/ui/editor/editor-visible-text-counter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { cn } from '@/components/common/ui/(shadcn)/lib/utils';

interface EditorVisibleTextCounterProps {
currentLength: number;
helperText?: string;
maxLength: number;
}

export default function EditorVisibleTextCounter({
currentLength,
helperText,
maxLength,
}: EditorVisibleTextCounterProps) {
return (
<div
className={cn(
'flex px-150 pt-50 pb-100',
helperText ? 'items-center justify-between gap-100' : 'justify-end',
)}
>
{helperText ? (
<p className="font-designer-13r text-text-subtlest">{helperText}</p>
) : null}
<p className="font-designer-13r text-text-subtlest">
{currentLength.toLocaleString()} / {maxLength.toLocaleString()}
</p>
</div>
);
}
18 changes: 18 additions & 0 deletions src/components/common/ui/editor/markdown-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ import {
import { cn } from '@/components/common/ui/(shadcn)/lib/utils';
import Button from '@/components/common/ui/button';
import { normalizeMarkdownContent } from '@/utils/markdown-content-normalize';
import { getRichContentVisibleTextLength } from '@/utils/markdown-content-text';
import { hasClipboardImageHint } from './clipboard-utils';
import EditorVisibleTextCounter from './editor-visible-text-counter';
import {
InstantCodeBlockExtension,
lowlight,
Expand Down Expand Up @@ -67,6 +69,10 @@ interface MarkdownEditorProps {
uploadImage?: (file: File) => Promise<string>;
normalizeContent?: (content: unknown) => string;
imageConfig?: MarkdownEditorImageConfig;
visibleTextCounter?: {
helperText?: string;
maxLength: number;
};
'aria-invalid'?: boolean;
'aria-describedby'?: string;
}
Expand All @@ -85,6 +91,7 @@ function MarkdownEditor({
uploadImage,
normalizeContent = normalizeMarkdownContent,
imageConfig,
visibleTextCounter,
'aria-invalid': ariaInvalid,
'aria-describedby': ariaDescribedBy,
}: MarkdownEditorProps) {
Expand All @@ -97,6 +104,9 @@ function MarkdownEditor({
const editorRef = useRef<Editor | null>(null);
const isInternalUpdate = useRef(false);
const normalizedValue = normalizeContent(value);
const currentVisibleTextLength = useMemo(() => {
return getRichContentVisibleTextLength(normalizedValue);
}, [normalizedValue]);

/**
* 유효한 에디터 인스턴스를 반환합니다.
Expand Down Expand Up @@ -705,6 +715,14 @@ function MarkdownEditor({
</div>
)}

{visibleTextCounter ? (
<EditorVisibleTextCounter
currentLength={currentVisibleTextLength}
helperText={visibleTextCounter.helperText}
maxLength={visibleTextCounter.maxLength}
/>
) : null}

{imageInsertError && (
<p className="font-designer-12r text-text-error px-150 pb-100">
{imageInsertError}
Expand Down
18 changes: 18 additions & 0 deletions src/components/common/ui/rich-text/markdown-editor-core.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -162,6 +168,7 @@ function MarkdownEditorCore({
value,
onChange,
placeholder,
visibleTextCounter,
allowedImageExtensions,
maxImageCount,
maxImageFileSize,
Expand All @@ -177,6 +184,9 @@ function MarkdownEditorCore({
(renderCount: number) => renderCount + 1,
0,
);
const currentVisibleTextLength = useMemo(() => {
return getRichContentVisibleTextLength(value);
}, [value]);

const allowedExtensionsLabel = useMemo(() => {
return allowedImageExtensions.join('/');
Expand Down Expand Up @@ -753,6 +763,14 @@ function MarkdownEditorCore({
</div>
)}

{visibleTextCounter ? (
<EditorVisibleTextCounter
currentLength={currentVisibleTextLength}
helperText={visibleTextCounter.helperText}
maxLength={visibleTextCounter.maxLength}
/>
) : null}

{imageInsertError && (
<p className="font-designer-12r text-text-error px-150 pb-100">
{imageInsertError}
Expand Down
10 changes: 5 additions & 5 deletions src/features/community/model/use-community-mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,8 @@ export const useCreateCommunityPostMutation = () => {
idempotencyKey: Parameters<typeof createCommunityPost>[1];
}) => createCommunityPost(request, idempotencyKey),
onError: () => {},
onSuccess: async () => {
await invalidateCommunityFeedQueries(queryClient);
onSuccess: () => {
invalidateCommunityFeedQueries(queryClient).catch(() => {});
},
});
};
Expand All @@ -315,11 +315,11 @@ export const useUpdateCommunityPostMutation = () => {
request: Parameters<typeof updateCommunityPost>[2];
}) => updateCommunityPost(postId, revision, request),
onError: () => {},
onSuccess: async (_, variables) => {
await Promise.all([
onSuccess: (_, variables) => {
Promise.all([
invalidateCommunityFeedQueries(queryClient),
invalidateCommunityPostQueries(queryClient, variables.postId),
]);
]).catch(() => {});
},
});
};
Expand Down
12 changes: 6 additions & 6 deletions src/features/community/model/use-community-qna-mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {});
},
});
};
Expand All @@ -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(() => {});
},
});
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,6 @@ export const useCommunityQnaQuestionWriteController = ({
request,
});

showToast('질문을 수정했습니다.');
router.push(buildCommunityQuestionHref(updatedQuestion.id, returnPage));

return;
Expand All @@ -207,7 +206,6 @@ export const useCommunityQnaQuestionWriteController = ({
idempotencyKey: createCommunityQnaIdempotencyKey('community-question'),
});

showToast('질문을 등록했습니다.');
router.push(buildCommunityQuestionHref(createdQuestion.id, returnPage));
} catch (error) {
const contentErrorMessage = getCommunityWriteContentErrorMessage(error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,6 @@ export const useCommunityWriteController = ({
},
});

showToast('글을 수정했습니다.');
router.push(buildCommunityPostHref(updatedPost.postId, returnPage));

return;
Expand All @@ -248,7 +247,6 @@ export const useCommunityWriteController = ({
createCommunityQnaIdempotencyKey('community-question'),
});

showToast('질문을 등록했습니다.');
router.push(buildCommunityQuestionHref(createdQuestion.id, returnPage));

return;
Expand All @@ -263,7 +261,6 @@ export const useCommunityWriteController = ({
idempotencyKey: createCommunityIdempotencyKey('community-post'),
});

showToast('글을 등록했습니다.');
router.push(buildCommunityPostHref(createdPost.postId, returnPage));
} catch (error) {
const contentErrorMessage = getCommunityWriteContentErrorMessage(error);
Expand Down
6 changes: 6 additions & 0 deletions src/features/community/ui/community-markdown-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
}
Expand All @@ -25,6 +29,7 @@ export default function CommunityMarkdownEditor({
value,
onChange,
placeholder,
visibleTextCounter,
requestImageUploadTicket = requestCommunityMarkdownImageUploadTicket,
uploadImageFile = uploadCommunityMarkdownImageFile,
}: CommunityMarkdownEditorProps) {
Expand All @@ -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}
Expand Down
30 changes: 30 additions & 0 deletions src/features/community/ui/community-meta-badge.test.ts
Original file line number Diff line number Diff line change
@@ -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문자',
});
});
});
22 changes: 11 additions & 11 deletions src/features/community/ui/community-meta-badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]: {
Expand All @@ -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<CommunityMemberRole>([
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -77,6 +78,9 @@ export default function CommunityQnaAnswerComposeSection({
value={field.value}
onChange={field.onChange}
placeholder="답변 내용을 작성해 주세요."
visibleTextCounter={{
maxLength: COMMUNITY_QNA_ANSWER_CONTENT_MAX_VISIBLE_LENGTH,
}}
/>
)}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -131,6 +134,9 @@ export default function CommunityQnaQuestionWritePageClient({
value={field.value}
onChange={field.onChange}
placeholder="질문 내용을 자세히 작성해 주세요."
visibleTextCounter={{
maxLength: COMMUNITY_WRITE_CONTENT_MAX_VISIBLE_LENGTH,
}}
/>
)}
/>
Expand Down
Loading
Loading