feat: 앱 메뉴관리 등록·수정 팝업에서 메뉴권한을 뺀다
셀렉트·검증·전송까지 걷어낸다. update는 백엔드가 동적 SET이라 기존 권한이 유지되고, 신규는 NULL로 들어간다. 목록의 메뉴권한 열은 그대로 둔다. Co-Authored-By: Claude Fable 5
@2bbe74270e72862a4daee5271d5d644f38ff71c3
--- app/(protected)/(basic)/system/menus/_actions.ts
+++ app/(protected)/(basic)/system/menus/_actions.ts
... | ... | @@ -62,7 +62,6 @@ |
| 62 | 62 |
parentIdPath: readString(formData, 'parentIdPath'), |
| 63 | 63 |
name: readString(formData, 'name'), |
| 64 | 64 |
sortOrder: parseFormNumber(formData, 'sortOrder'), |
| 65 |
- roleId: readString(formData, 'roleId'), |
|
| 66 | 65 |
isTopActive: readString(formData, 'isTopActive') === 'Y', |
| 67 | 66 |
url: readString(formData, 'url'), |
| 68 | 67 |
description: readString(formData, 'description'), |
--- app/(protected)/(basic)/system/menus/_components/menu-form-modal.tsx
+++ app/(protected)/(basic)/system/menus/_components/menu-form-modal.tsx
... | ... | @@ -7,7 +7,6 @@ |
| 7 | 7 |
import { FoxHelperText } from '@fox/core/components/fox-helper-text';
|
| 8 | 8 |
import { FoxInput } from '@fox/core/components/fox-input';
|
| 9 | 9 |
import { FoxModal } from '@fox/core/components/fox-modal';
|
| 10 |
-import { FoxSelect } from '@fox/core/components/fox-select';
|
|
| 11 | 10 |
import { FoxToggleSwitch } from '@fox/core/components/fox-toggle-switch';
|
| 12 | 11 |
import { submitFormAction } from '@/app/_hooks/submit-form-action';
|
| 13 | 12 |
import { useFeedback } from '@/app/_hooks/use-feedback';
|
... | ... | @@ -33,7 +32,6 @@ |
| 33 | 32 |
menu?: SystemMenuNode; |
| 34 | 33 |
/** 추가일 때의 상위 메뉴. 수정일 때는 대상의 상위다. */ |
| 35 | 34 |
parent: SystemMenu | null; |
| 36 |
- roleOptions: { value: string; label: string }[];
|
|
| 37 | 35 |
onClose: () => void; |
| 38 | 36 |
} |
| 39 | 37 |
|
... | ... | @@ -41,13 +39,7 @@ |
| 41 | 39 |
const ICON_HINT = |
| 42 | 40 |
'이미지 파일만 업로드 가능 (jpg, png, svg 등). 앱 화면에서 메뉴 표시 시 사용됩니다.'; |
| 43 | 41 |
|
| 44 |
-export function MenuFormModal({
|
|
| 45 |
- mode, |
|
| 46 |
- menu, |
|
| 47 |
- parent, |
|
| 48 |
- roleOptions, |
|
| 49 |
- onClose, |
|
| 50 |
-}: MenuFormModalProps) {
|
|
| 42 |
+export function MenuFormModal({ mode, menu, parent, onClose }: MenuFormModalProps) {
|
|
| 51 | 43 |
const isCreate = mode === 'create'; |
| 52 | 44 |
const { showToast } = useFeedback();
|
| 53 | 45 |
const formRef = useRef<HTMLFormElement>(null); |
... | ... | @@ -56,7 +48,6 @@ |
| 56 | 48 |
INITIAL_SYSTEM_MENU_FORM_STATE |
| 57 | 49 |
); |
| 58 | 50 |
|
| 59 |
- const [roleId, setRoleId] = useState(menu?.roleId ?? roleOptions[0]?.value ?? ''); |
|
| 60 | 51 |
const [isTopActive, setIsTopActive] = useState(menu?.isTopActive ?? false); |
| 61 | 52 |
const [isActive, setIsActive] = useState(menu?.isActive ?? true); |
| 62 | 53 |
// 새창여부는 저장할 컬럼이 없어 화면에만 둔다(사용자 확정). |
... | ... | @@ -111,7 +102,6 @@ |
| 111 | 102 |
<input type="hidden" name="id" value={menu?.id ?? ''} />
|
| 112 | 103 |
<input type="hidden" name="parentId" value={parent?.id ?? ''} />
|
| 113 | 104 |
<input type="hidden" name="parentIdPath" value={buildChildIdPath(parent)} />
|
| 114 |
- <input type="hidden" name="roleId" value={roleId} />
|
|
| 115 | 105 |
<input type="hidden" name="isTopActive" value={isTopActive ? 'Y' : 'N'} />
|
| 116 | 106 |
<input type="hidden" name="isActive" value={isActive ? 'Y' : 'N'} />
|
| 117 | 107 |
<input type="hidden" name="largeIconId" value={largeIconId} />
|
... | ... | @@ -160,19 +150,6 @@ |
| 160 | 150 |
message={errors.sortOrder}
|
| 161 | 151 |
/> |
| 162 | 152 |
</div> |
| 163 |
- </div> |
|
| 164 |
- |
|
| 165 |
- <div className={styles.field}>
|
|
| 166 |
- <FoxSelect |
|
| 167 |
- size="md" |
|
| 168 |
- label="메뉴권한" |
|
| 169 |
- requirement="required" |
|
| 170 |
- options={roleOptions}
|
|
| 171 |
- value={roleId}
|
|
| 172 |
- onValueChange={setRoleId}
|
|
| 173 |
- error={Boolean(errors.roleId)}
|
|
| 174 |
- hint={errors.roleId}
|
|
| 175 |
- /> |
|
| 176 | 153 |
</div> |
| 177 | 154 |
|
| 178 | 155 |
<div className={styles.field}>
|
--- app/(protected)/(basic)/system/menus/_components/menu-tree-list.tsx
+++ app/(protected)/(basic)/system/menus/_components/menu-tree-list.tsx
... | ... | @@ -9,7 +9,6 @@ |
| 9 | 9 |
import {
|
| 10 | 10 |
buildMenuTree, |
| 11 | 11 |
collectNodeIds, |
| 12 |
- collectRoleOptions, |
|
| 13 | 12 |
flattenOpenNodes, |
| 14 | 13 |
formatActiveLabel, |
| 15 | 14 |
formatOptionalValue, |
... | ... | @@ -33,7 +32,6 @@ |
| 33 | 32 |
|
| 34 | 33 |
export function MenuTreeList({ items }: MenuTreeListProps) {
|
| 35 | 34 |
const tree = useMemo(() => buildMenuTree(items), [items]); |
| 36 |
- const roleOptions = useMemo(() => collectRoleOptions(items), [items]); |
|
| 37 | 35 |
const byId = useMemo(() => new Map(items.map((item) => [item.id, item])), [items]); |
| 38 | 36 |
|
| 39 | 37 |
const [closedIds, setClosedIds] = useState<Set<string>>(new Set()); |
... | ... | @@ -167,7 +165,6 @@ |
| 167 | 165 |
? (target.node.parentId ? (byId.get(target.node.parentId) ?? null) : null) |
| 168 | 166 |
: target.parent |
| 169 | 167 |
} |
| 170 |
- roleOptions={roleOptions}
|
|
| 171 | 168 |
onClose={() => setTarget(null)}
|
| 172 | 169 |
/> |
| 173 | 170 |
)} |
--- lib/data/repositories/system-menu-repository.ts
+++ lib/data/repositories/system-menu-repository.ts
... | ... | @@ -59,7 +59,6 @@ |
| 59 | 59 |
name: readString(raw, 'menuName') ?? '', |
| 60 | 60 |
idPath: readString(raw, 'menuIdPath'), |
| 61 | 61 |
url: readString(raw, 'menuUrl'), |
| 62 |
- roleId: readString(raw, 'roleId'), |
|
| 63 | 62 |
roleName: readString(raw, 'roleNm'), |
| 64 | 63 |
sortOrder: Number.isInteger(Number(raw.sortOrdr)) ? Number(raw.sortOrdr) : 0, |
| 65 | 64 |
isActive: parseYesNo(raw.useYn), |
... | ... | @@ -99,12 +98,15 @@ |
| 99 | 98 |
return data.list.flatMap(toSystemMenu); |
| 100 | 99 |
}); |
| 101 | 100 |
|
| 102 |
-/** 새창여부는 보내지 않는다 — 저장할 컬럼이 없다(파일 상단 ⚠️ 3). */ |
|
| 101 |
+/** |
|
| 102 |
+ * 새창여부는 보내지 않는다 — 저장할 컬럼이 없다(파일 상단 ⚠️ 3). |
|
| 103 |
+ * 메뉴권한(roleId)도 화면에서 빠져 보내지 않는다 — update는 동적 SET이라 기존 값이 유지되고, |
|
| 104 |
+ * 신규는 NULL로 들어간다(권한 없는 메뉴는 원래도 있다). |
|
| 105 |
+ */ |
|
| 103 | 106 |
function toWriteParams(values: SystemMenuValues) {
|
| 104 | 107 |
return {
|
| 105 | 108 |
menuName: values.name, |
| 106 | 109 |
sortOrdr: values.sortOrder, |
| 107 |
- roleId: values.roleId, |
|
| 108 | 110 |
topYn: values.isTopActive ? 'Y' : 'N', |
| 109 | 111 |
menuUrl: values.url, |
| 110 | 112 |
menuExplan: values.description, |
--- lib/domain/system-menu-form.ts
+++ lib/domain/system-menu-form.ts
... | ... | @@ -9,7 +9,6 @@ |
| 9 | 9 |
parentIdPath: string; |
| 10 | 10 |
name: string; |
| 11 | 11 |
sortOrder: number; |
| 12 |
- roleId: string; |
|
| 13 | 12 |
isTopActive: boolean; |
| 14 | 13 |
url: string; |
| 15 | 14 |
description: string; |
... | ... | @@ -20,7 +19,7 @@ |
| 20 | 19 |
|
| 21 | 20 |
export type SystemMenuFormErrors = Partial< |
| 22 | 21 |
Record< |
| 23 |
- 'name' | 'sortOrder' | 'roleId' | 'url' | 'description' | 'largeIconId' | 'smallIconId', |
|
| 22 |
+ 'name' | 'sortOrder' | 'url' | 'description' | 'largeIconId' | 'smallIconId', |
|
| 24 | 23 |
string |
| 25 | 24 |
> |
| 26 | 25 |
>; |
... | ... | @@ -79,10 +78,6 @@ |
| 79 | 78 |
errors.description = description.error; |
| 80 | 79 |
} |
| 81 | 80 |
|
| 82 |
- if (!values.roleId.trim()) {
|
|
| 83 |
- errors.roleId = REQUIRED_MESSAGE; |
|
| 84 |
- } |
|
| 85 |
- |
|
| 86 | 81 |
if (!Number.isInteger(values.sortOrder) || values.sortOrder < 1) {
|
| 87 | 82 |
errors.sortOrder = '메뉴순번은 1 이상의 숫자로 입력해 주세요.'; |
| 88 | 83 |
} else if (values.sortOrder > SORT_ORDER_MAX) {
|
... | ... | @@ -100,7 +95,6 @@ |
| 100 | 95 |
name: name.value, |
| 101 | 96 |
url: url.value, |
| 102 | 97 |
description: description.value, |
| 103 |
- roleId: values.roleId.trim(), |
|
| 104 | 98 |
}, |
| 105 | 99 |
}; |
| 106 | 100 |
} |
--- lib/domain/system-menu.ts
+++ lib/domain/system-menu.ts
... | ... | @@ -4,7 +4,6 @@ |
| 4 | 4 |
name: string; |
| 5 | 5 |
idPath: string | null; |
| 6 | 6 |
url: string | null; |
| 7 |
- roleId: string | null; |
|
| 8 | 7 |
roleName: string | null; |
| 9 | 8 |
sortOrder: number; |
| 10 | 9 |
isActive: boolean; |
... | ... | @@ -104,26 +103,6 @@ |
| 104 | 103 |
|
| 105 | 104 |
walk(nodes); |
| 106 | 105 |
return ids; |
| 107 |
-} |
|
| 108 |
- |
|
| 109 |
-/** |
|
| 110 |
- * 메뉴권한 셀렉트의 선택지 — 역할 목록 API가 없어 목록 응답에 실제로 쓰인 역할만 모은다 |
|
| 111 |
- * (사용자 확정 사항). |
|
| 112 |
- */ |
|
| 113 |
-export function collectRoleOptions( |
|
| 114 |
- items: SystemMenu[] |
|
| 115 |
-): { value: string; label: string }[] {
|
|
| 116 |
- const byId = new Map<string, string>(); |
|
| 117 |
- |
|
| 118 |
- for (const item of items) {
|
|
| 119 |
- if (item.roleId && !byId.has(item.roleId)) {
|
|
| 120 |
- byId.set(item.roleId, item.roleName ?? item.roleId); |
|
| 121 |
- } |
|
| 122 |
- } |
|
| 123 |
- |
|
| 124 |
- return [...byId.entries()] |
|
| 125 |
- .map(([value, label]) => ({ value, label }))
|
|
| 126 |
- .sort((a, b) => a.label.localeCompare(b.label, 'ko-KR')); |
|
| 127 | 106 |
} |
| 128 | 107 |
|
| 129 | 108 |
/** |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?