Skip to content

Commit 4c98932

Browse files
committed
refactor: 엔터 한 번에 태그 등록되도록 수정
1 parent 4db3302 commit 4c98932

1 file changed

Lines changed: 44 additions & 10 deletions

File tree

frontend/src/components/node-datail/fields/TagField.tsx

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
'use client';
22

3+
import { useQueryClient } from '@tanstack/react-query';
34
import { ContentBadge, ThemeColorsToken, Typography } from '@wanteddev/wds';
45
import { IconClose } from '@wanteddev/wds-icon';
6+
import { useRef } from 'react';
57

68
import { TagItem } from '@/api/Api';
79
import { ColorType } from '@/constants/badgeColor';
810
import { useErrorToast } from '@/hooks/useErrorToast';
11+
import { tagKeys } from '@/queries/keys/tagKeys';
912
import {
1013
useAddNodeTagMutation,
1114
useCreateTagMutation,
@@ -45,15 +48,19 @@ export function TagField({ projectId, nodeId, initialTags }: TagFieldProps) {
4548
setOutsideCloseDisabled,
4649
} = usePickerState();
4750

51+
const queryClient = useQueryClient();
4852
const showErrorToast = useErrorToast();
4953
const { tags, yAddTag, yRemoveTag } = useYjsTags(initialTags);
5054
const { data: allTags = [] } = useProjectTagsQuery(projectId);
5155
const { mutate: addTag } = useAddNodeTagMutation(projectId, nodeId);
5256
const { mutate: removeTag } = useRemoveNodeTagMutation(projectId, nodeId);
53-
const { mutate: createTag, isPending: isCreating } = useCreateTagMutation(projectId);
57+
const { mutate: createTag } = useCreateTagMutation(projectId);
5458
const { mutate: deleteTag } = useDeleteTagMutation(projectId);
5559
const { mutate: updateTagColor } = useUpdateTagColorMutation(projectId);
5660

61+
const isSubmittingRef = useRef(false);
62+
const pendingEnterRef = useRef(false);
63+
5764
const assignedTagIds = new Set(tags.map((t) => t.tagId));
5865

5966
const handleAdd = (tag: TagItem) => {
@@ -121,30 +128,52 @@ export function TagField({ projectId, nodeId, initialTags }: TagFieldProps) {
121128
!allTags.some((t) => t.name?.toLowerCase() === inputValue.trim().toLowerCase());
122129
const totalItems = filteredTags.length + (canCreate ? 1 : 0);
123130

124-
const handleCreateTag = () => {
125-
const trimmed = inputValue.trim();
126-
if (!trimmed || isCreating) return;
131+
const handleCreateTag = (overrideValue?: string) => {
132+
if (isSubmittingRef.current) return;
133+
const trimmed = (overrideValue ?? inputValue).trim();
134+
if (!trimmed) return;
127135

128136
const exactMatch = allTags.find((t) => t.name?.toLowerCase() === trimmed.toLowerCase());
129137
if (exactMatch) {
130138
if (!assignedTagIds.has(exactMatch.tagId)) handleAdd(exactMatch);
131139
return;
132140
}
133141

142+
isSubmittingRef.current = true;
134143
createTag(
135144
{ name: trimmed, color: randomColor() },
136145
{
137-
onSuccess: (newTag) => {
138-
if (!newTag?.tagId) return;
139-
handleAdd(newTag);
146+
onSuccess: async (newTag) => {
147+
isSubmittingRef.current = false;
148+
if (newTag?.tagId) {
149+
handleAdd(newTag);
150+
return;
151+
}
152+
// API가 tagId를 응답에 포함하지 않는 경우: tags 목록을 refetch 후 이름으로 탐색
153+
await queryClient.refetchQueries({ queryKey: tagKeys.list(projectId) });
154+
const freshTags = queryClient.getQueryData<TagItem[]>(tagKeys.list(projectId));
155+
const created = freshTags?.find((t) => t.name?.toLowerCase() === trimmed.toLowerCase());
156+
if (created?.tagId) handleAdd(created);
157+
},
158+
onError: (err) => {
159+
isSubmittingRef.current = false;
160+
showErrorToast(err, '태그 생성에 실패했어요.');
140161
},
141-
onError: (err) => showErrorToast(err, '태그 생성에 실패했어요.'),
142162
},
143163
);
144164
};
145165

166+
const handleCompositionEnd = (e: React.CompositionEvent<HTMLInputElement>) => {
167+
if (!pendingEnterRef.current) return;
168+
pendingEnterRef.current = false;
169+
if (selectedIndex >= 0 && selectedIndex < filteredTags.length) {
170+
handleAdd(filteredTags[selectedIndex]);
171+
} else {
172+
handleCreateTag(e.currentTarget.value);
173+
}
174+
};
175+
146176
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
147-
if (e.nativeEvent.isComposing) return;
148177
if (e.key === 'ArrowDown') {
149178
e.preventDefault();
150179
setSelectedIndex((prev) => Math.min(prev + 1, totalItems - 1));
@@ -153,6 +182,10 @@ export function TagField({ projectId, nodeId, initialTags }: TagFieldProps) {
153182
setSelectedIndex((prev) => Math.max(prev - 1, -1));
154183
} else if (e.key === 'Enter') {
155184
e.preventDefault();
185+
if (e.nativeEvent.isComposing) {
186+
pendingEnterRef.current = true;
187+
return;
188+
}
156189
if (selectedIndex >= 0 && selectedIndex < filteredTags.length) {
157190
handleAdd(filteredTags[selectedIndex]);
158191
} else if (selectedIndex === filteredTags.length && canCreate) {
@@ -202,6 +235,7 @@ export function TagField({ projectId, nodeId, initialTags }: TagFieldProps) {
202235
value={inputValue}
203236
onChange={handleInputChange}
204237
onKeyDown={handleKeyDown}
238+
onCompositionEnd={handleCompositionEnd}
205239
className="text-label-normal min-w-20 flex-1 border-0 bg-transparent text-sm outline-none"
206240
onClick={(e) => e.stopPropagation()}
207241
/>
@@ -250,7 +284,7 @@ export function TagField({ projectId, nodeId, initialTags }: TagFieldProps) {
250284
<button
251285
type="button"
252286
onMouseDown={(e) => e.preventDefault()}
253-
onClick={handleCreateTag}
287+
onClick={() => handleCreateTag()}
254288
className={`text-label-alternative flex w-full items-center gap-2 px-3 py-2 ${selectedIndex === filteredTags.length ? 'bg-gray-100' : 'bg-white hover:bg-gray-50'}`}
255289
>
256290
<Typography variant="caption1">

0 commit comments

Comments
 (0)