임동욱 임동욱 09-04
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
+++ app/(protected)/(basic)/system/menus/_actions.ts
@@ -62,7 +62,6 @@
     parentIdPath: readString(formData, 'parentIdPath'),
     name: readString(formData, 'name'),
     sortOrder: parseFormNumber(formData, 'sortOrder'),
-    roleId: readString(formData, 'roleId'),
     isTopActive: readString(formData, 'isTopActive') === 'Y',
     url: readString(formData, 'url'),
     description: readString(formData, 'description'),
app/(protected)/(basic)/system/menus/_components/menu-form-modal.tsx
--- app/(protected)/(basic)/system/menus/_components/menu-form-modal.tsx
+++ app/(protected)/(basic)/system/menus/_components/menu-form-modal.tsx
@@ -7,7 +7,6 @@
 import { FoxHelperText } from '@fox/core/components/fox-helper-text';
 import { FoxInput } from '@fox/core/components/fox-input';
 import { FoxModal } from '@fox/core/components/fox-modal';
-import { FoxSelect } from '@fox/core/components/fox-select';
 import { FoxToggleSwitch } from '@fox/core/components/fox-toggle-switch';
 import { submitFormAction } from '@/app/_hooks/submit-form-action';
 import { useFeedback } from '@/app/_hooks/use-feedback';
@@ -33,7 +32,6 @@
   menu?: SystemMenuNode;
   /** 추가일 때의 상위 메뉴. 수정일 때는 대상의 상위다. */
   parent: SystemMenu | null;
-  roleOptions: { value: string; label: string }[];
   onClose: () => void;
 }
 
@@ -41,13 +39,7 @@
 const ICON_HINT =
   '이미지 파일만 업로드 가능 (jpg, png, svg 등). 앱 화면에서 메뉴 표시 시 사용됩니다.';
 
-export function MenuFormModal({
-  mode,
-  menu,
-  parent,
-  roleOptions,
-  onClose,
-}: MenuFormModalProps) {
+export function MenuFormModal({ mode, menu, parent, onClose }: MenuFormModalProps) {
   const isCreate = mode === 'create';
   const { showToast } = useFeedback();
   const formRef = useRef<HTMLFormElement>(null);
@@ -56,7 +48,6 @@
     INITIAL_SYSTEM_MENU_FORM_STATE
   );
 
-  const [roleId, setRoleId] = useState(menu?.roleId ?? roleOptions[0]?.value ?? '');
   const [isTopActive, setIsTopActive] = useState(menu?.isTopActive ?? false);
   const [isActive, setIsActive] = useState(menu?.isActive ?? true);
   // 새창여부는 저장할 컬럼이 없어 화면에만 둔다(사용자 확정).
@@ -111,7 +102,6 @@
         <input type="hidden" name="id" value={menu?.id ?? ''} />
         <input type="hidden" name="parentId" value={parent?.id ?? ''} />
         <input type="hidden" name="parentIdPath" value={buildChildIdPath(parent)} />
-        <input type="hidden" name="roleId" value={roleId} />
         <input type="hidden" name="isTopActive" value={isTopActive ? 'Y' : 'N'} />
         <input type="hidden" name="isActive" value={isActive ? 'Y' : 'N'} />
         <input type="hidden" name="largeIconId" value={largeIconId} />
@@ -160,19 +150,6 @@
               message={errors.sortOrder}
             />
           </div>
-        </div>
-
-        <div className={styles.field}>
-          <FoxSelect
-            size="md"
-            label="메뉴권한"
-            requirement="required"
-            options={roleOptions}
-            value={roleId}
-            onValueChange={setRoleId}
-            error={Boolean(errors.roleId)}
-            hint={errors.roleId}
-          />
         </div>
 
         <div className={styles.field}>
app/(protected)/(basic)/system/menus/_components/menu-tree-list.tsx
--- app/(protected)/(basic)/system/menus/_components/menu-tree-list.tsx
+++ app/(protected)/(basic)/system/menus/_components/menu-tree-list.tsx
@@ -9,7 +9,6 @@
 import {
   buildMenuTree,
   collectNodeIds,
-  collectRoleOptions,
   flattenOpenNodes,
   formatActiveLabel,
   formatOptionalValue,
@@ -33,7 +32,6 @@
 
 export function MenuTreeList({ items }: MenuTreeListProps) {
   const tree = useMemo(() => buildMenuTree(items), [items]);
-  const roleOptions = useMemo(() => collectRoleOptions(items), [items]);
   const byId = useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);
 
   const [closedIds, setClosedIds] = useState<Set<string>>(new Set());
@@ -167,7 +165,6 @@
               ? (target.node.parentId ? (byId.get(target.node.parentId) ?? null) : null)
               : target.parent
           }
-          roleOptions={roleOptions}
           onClose={() => setTarget(null)}
         />
       )}
lib/data/repositories/system-menu-repository.ts
--- lib/data/repositories/system-menu-repository.ts
+++ lib/data/repositories/system-menu-repository.ts
@@ -59,7 +59,6 @@
       name: readString(raw, 'menuName') ?? '',
       idPath: readString(raw, 'menuIdPath'),
       url: readString(raw, 'menuUrl'),
-      roleId: readString(raw, 'roleId'),
       roleName: readString(raw, 'roleNm'),
       sortOrder: Number.isInteger(Number(raw.sortOrdr)) ? Number(raw.sortOrdr) : 0,
       isActive: parseYesNo(raw.useYn),
@@ -99,12 +98,15 @@
   return data.list.flatMap(toSystemMenu);
 });
 
-/** 새창여부는 보내지 않는다 — 저장할 컬럼이 없다(파일 상단 ⚠️ 3). */
+/**
+ * 새창여부는 보내지 않는다 — 저장할 컬럼이 없다(파일 상단 ⚠️ 3).
+ * 메뉴권한(roleId)도 화면에서 빠져 보내지 않는다 — update는 동적 SET이라 기존 값이 유지되고,
+ * 신규는 NULL로 들어간다(권한 없는 메뉴는 원래도 있다).
+ */
 function toWriteParams(values: SystemMenuValues) {
   return {
     menuName: values.name,
     sortOrdr: values.sortOrder,
-    roleId: values.roleId,
     topYn: values.isTopActive ? 'Y' : 'N',
     menuUrl: values.url,
     menuExplan: values.description,
lib/domain/system-menu-form.ts
--- lib/domain/system-menu-form.ts
+++ lib/domain/system-menu-form.ts
@@ -9,7 +9,6 @@
   parentIdPath: string;
   name: string;
   sortOrder: number;
-  roleId: string;
   isTopActive: boolean;
   url: string;
   description: string;
@@ -20,7 +19,7 @@
 
 export type SystemMenuFormErrors = Partial<
   Record<
-    'name' | 'sortOrder' | 'roleId' | 'url' | 'description' | 'largeIconId' | 'smallIconId',
+    'name' | 'sortOrder' | 'url' | 'description' | 'largeIconId' | 'smallIconId',
     string
   >
 >;
@@ -79,10 +78,6 @@
     errors.description = description.error;
   }
 
-  if (!values.roleId.trim()) {
-    errors.roleId = REQUIRED_MESSAGE;
-  }
-
   if (!Number.isInteger(values.sortOrder) || values.sortOrder < 1) {
     errors.sortOrder = '메뉴순번은 1 이상의 숫자로 입력해 주세요.';
   } else if (values.sortOrder > SORT_ORDER_MAX) {
@@ -100,7 +95,6 @@
       name: name.value,
       url: url.value,
       description: description.value,
-      roleId: values.roleId.trim(),
     },
   };
 }
lib/domain/system-menu.ts
--- lib/domain/system-menu.ts
+++ lib/domain/system-menu.ts
@@ -4,7 +4,6 @@
   name: string;
   idPath: string | null;
   url: string | null;
-  roleId: string | null;
   roleName: string | null;
   sortOrder: number;
   isActive: boolean;
@@ -104,26 +103,6 @@
 
   walk(nodes);
   return ids;
-}
-
-/**
- * 메뉴권한 셀렉트의 선택지 — 역할 목록 API가 없어 목록 응답에 실제로 쓰인 역할만 모은다
- * (사용자 확정 사항).
- */
-export function collectRoleOptions(
-  items: SystemMenu[]
-): { value: string; label: string }[] {
-  const byId = new Map<string, string>();
-
-  for (const item of items) {
-    if (item.roleId && !byId.has(item.roleId)) {
-      byId.set(item.roleId, item.roleName ?? item.roleId);
-    }
-  }
-
-  return [...byId.entries()]
-    .map(([value, label]) => ({ value, label }))
-    .sort((a, b) => a.label.localeCompare(b.label, 'ko-KR'));
 }
 
 /**
Add a comment
List