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
6 changes: 5 additions & 1 deletion src/features/mentoring/api/mentor-api.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ export interface MentorSavedCoreKeywordResponseDto {
label?: string;
}

export type MentorReadableCoreKeywordResponseDto =
| CodeLabelResponseDto
| MentorSavedCoreKeywordResponseDto;

export interface MentorCareerEntryResponseDto {
description?: string;
isCurrent?: boolean;
Expand All @@ -122,7 +126,7 @@ export interface ProfileResponseDto {
careerCode?: string;
careerYears?: string;
careerEntries?: MentorCareerEntryResponseDto[];
coreKeywords?: MentorSavedCoreKeywordResponseDto[];
coreKeywords?: MentorReadableCoreKeywordResponseDto[];
skillTags?: string[];
company?: CompanyResponseDto;
}
Expand Down
129 changes: 72 additions & 57 deletions src/features/mentoring/api/mentor-core-keyword-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from './mentor-api-contract';
import type {
MentorCoreKeywordRequestDto,
MentorReadableCoreKeywordResponseDto,
MentorSavedCoreKeywordResponseDto,
} from './mentor-api.types';

Expand Down Expand Up @@ -38,6 +39,70 @@ const normalizeSavedCoreKeywordType = ({
});
};

const toTrimmedString = (value: unknown) => {
return typeof value === 'string' ? value.trim() : '';
};

const readMentorCoreKeyword = ({
value,
scope,
field,
}: {
value: unknown;
scope: MentorApiContractScope;
field: string;
}): MentorCoreKeywordSnapshot => {
const keyword = requireObject<MentorReadableCoreKeywordResponseDto>({
value,
scope,
field,
});
const label = requireNonEmptyString({
value: keyword.label,
scope,
field: `${field}.label`,
});
const code = toTrimmedString(keyword.code);

if ('type' in keyword && keyword.type !== undefined) {
const type = normalizeSavedCoreKeywordType({
value: keyword.type,
scope,
field: `${field}.type`,
});

if (type === 'PREDEFINED') {
return {
type,
code: requireNonEmptyString({
value: keyword.code,
scope,
field: `${field}.code`,
}),
label,
};
}

return {
type,
label,
};
}

if (code.length > 0) {
return {
type: 'PREDEFINED',
code,
label,
};
}

return {
type: 'CUSTOM',
label,
};
};

export const requireMentorCoreKeywordFormValues = ({
value,
scope,
Expand All @@ -54,30 +119,17 @@ export const requireMentorCoreKeywordFormValues = ({
});

return keywords.map((item, index) => {
const keyword = requireObject<MentorSavedCoreKeywordResponseDto>({
const keyword = readMentorCoreKeyword({
value: item,
scope,
field: `${field}[${index}]`,
});
const type = normalizeSavedCoreKeywordType({
value: keyword.type,
scope,
field: `${field}[${index}].type`,
});

if (type === 'PREDEFINED') {
return requireNonEmptyString({
value: keyword.code,
scope,
field: `${field}[${index}].code`,
});
if (keyword.type === 'PREDEFINED' && keyword.code) {
return keyword.code;
}

return requireNonEmptyString({
value: keyword.label,
scope,
field: `${field}[${index}].label`,
});
return keyword.label;
});
};

Expand All @@ -97,38 +149,11 @@ export const requireMentorCoreKeywordSnapshots = ({
});

return keywords.map((item, index) => {
const keyword = requireObject<MentorSavedCoreKeywordResponseDto>({
return readMentorCoreKeyword({
value: item,
scope,
field: `${field}[${index}]`,
});
const type = normalizeSavedCoreKeywordType({
value: keyword.type,
scope,
field: `${field}[${index}].type`,
});
const label = requireNonEmptyString({
value: keyword.label,
scope,
field: `${field}[${index}].label`,
});

if (type === 'PREDEFINED') {
return {
type,
code: requireNonEmptyString({
value: keyword.code,
scope,
field: `${field}[${index}].code`,
}),
label,
};
}

return {
type,
label,
};
});
};

Expand All @@ -148,23 +173,13 @@ export const requireMentorCoreKeywordLabels = ({
});

return keywords.map((item, index) => {
const keyword = requireObject<MentorSavedCoreKeywordResponseDto>({
const keyword = readMentorCoreKeyword({
value: item,
scope,
field: `${field}[${index}]`,
});

normalizeSavedCoreKeywordType({
value: keyword.type,
scope,
field: `${field}[${index}].type`,
});

return requireNonEmptyString({
value: keyword.label,
scope,
field: `${field}[${index}].label`,
});
return keyword.label;
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1329,7 +1329,7 @@ export const useMentorRegistrationController =
? '저장은 완료되었고 신청 가능 상태로 반영됩니다'
: isDetailPreparing
? '저장은 완료되었지만 공개 준비가 더 필요합니다'
: '저장은 완료되었고 상세 공개 준비 상태로 반영됩니다',
: '저장은 완료되었고 상세 공개 준비 상태로 반영됩니다',
description: finalizedValues.listVisible
? isApplyReady
? '목록 공개가 켜져 있으며 저장된 멘토링 정보가 신청 가능 상태로 반영됩니다.'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ export default function MentorCareerEntriesEditor({
typeof errors.careerEntries?.message === 'string'
? errors.careerEntries.message
: undefined;
const currentCareerEntryIndex = watchedCareerEntries.findIndex(
(entry) => entry?.isCurrent === true,
);
const splitCareerEntryMonth = (value?: string) => {
if (!value || !value.includes('-')) {
return {
Expand Down Expand Up @@ -204,16 +207,32 @@ export default function MentorCareerEntriesEditor({
index: number;
nextIsCurrent: boolean;
}) => {
if (nextIsCurrent) {
setValue(`careerEntries.${index}.endMonth`, '', DIRTY_VALIDATION_OPTIONS);
clearErrors(`careerEntries.${index}.endMonth`);
}

setValue(
`careerEntries.${index}.isCurrent`,
nextIsCurrent,
'careerEntries',
watchedCareerEntries.map((entry, entryIndex) => {
const currentEntry = entry ?? createEmptyMentorCareerEntry();

if (entryIndex !== index) {
return nextIsCurrent
? {
...currentEntry,
isCurrent: false,
}
: currentEntry;
}

return {
...currentEntry,
isCurrent: nextIsCurrent,
endMonth: nextIsCurrent ? '' : (currentEntry.endMonth ?? ''),
};
}),
DIRTY_VALIDATION_OPTIONS,
);

if (nextIsCurrent) {
clearErrors([`careerEntries.${index}.endMonth`, 'careerEntries']);
}
Comment on lines +233 to +235

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In react-hook-form, does clearErrors('careerEntries') also clear nested errors like careerEntries.0.description and careerEntries.0.endMonth, or only the root array error?

💡 Result:

In react-hook-form, calling clearErrors('careerEntries') clears both the root array error (if any) and all nested errors like careerEntries.0.description and careerEntries.0.endMonth. This is the expected behavior, as confirmed by the official documentation and GitHub issues. The official clearErrors documentation explicitly states: register("test.firstName", { required: true }) register("test.lastName", { required: true }) clearErrors("test") // will clear both errors from test.firstName and test.lastName clearErrors("test.firstName") // for clear single input error This demonstrates that clearing a parent field name removes all errors under that path, including nested ones. GitHub issue #8476 confirms this for field arrays: maintainer @bluebill1049 states "This is expected behavior" when clearErrors on a field array root clears nested field errors. They note there's no built-in way to clear only the root without affecting nested errors—you must specify exact nested paths. This behavior has been consistent across versions, with fixes ensuring clearErrors on a path clears the entire subtree (e.g., issues #2730, #2961).

Citations:


clearErrors('careerEntries')로 다른 행의 유효성 검사 오류까지 함께 제거됩니다.

clearErrors('careerEntries')는 배열 루트 오류뿐만 아니라 careerEntries.0.description, careerEntries.0.endMonth 같은 중첩 필드의 모든 오류를 함께 제거합니다. 바로 앞의 setValue(..., { shouldValidate: true })로 재계산된 다른 행의 유효성 검사 오류가 숨겨져서 검증 흐름이 불일치할 수 있습니다.

수정안
-    if (nextIsCurrent) {
-      clearErrors([`careerEntries.${index}.endMonth`, 'careerEntries']);
-    }
+    if (nextIsCurrent) {
+      clearErrors(`careerEntries.${index}.endMonth`);
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/mentoring/ui/registration/mentor-career-entries-editor.tsx`
around lines 233 - 235, 현재 clearErrors('careerEntries') 호출이 배열 루트와 그 하위 모든
필드(careerEntries.*)의 오류를 전부 제거하여, 바로 앞 setValue(..., { shouldValidate: true })로
재검증된 다른 행의 오류까지 숨기고 검증 흐름을 망가뜨립니다; 이를 고치려면 clearErrors 호출을 범위를 좁혀서 현재 행의
endMonth와 (필요시) 해당 행의 관련 필드만 지우도록 변경하세요(예:
clearErrors([`careerEntries.${index}.endMonth`]) 대신 전체 루트를 지우지 않음), 관련 심볼:
clearErrors, setValue, careerEntries, index, nextIsCurrent를 찾아 위와 같이 대상 필드만
제거되도록 수정하세요.

};
const handleRemove = (index: number) => {
remove(index);
Expand Down Expand Up @@ -272,6 +291,10 @@ export default function MentorCareerEntriesEditor({
: undefined;
const isPeriodEditorOpen = currentEntry?.periodEnabled === true;
const isCurrentCareerEntry = currentEntry?.isCurrent === true;
const isAnotherCurrentCareerEntrySelected =
currentCareerEntryIndex >= 0 &&
currentCareerEntryIndex !== index &&
!isCurrentCareerEntry;
const startMonthOptions =
startParts.year === String(currentCareerEntryYear)
? monthOptions.filter(
Expand Down Expand Up @@ -341,6 +364,7 @@ export default function MentorCareerEntriesEditor({
type="checkbox"
className="border-border-default rounded-50 accent-fill-brand-default-default size-200 border"
checked={isCurrentCareerEntry}
disabled={isAnotherCurrentCareerEntrySelected}
onChange={(event) =>
handleIsCurrentChange({
index,
Expand Down
Loading