임동욱 임동욱 08-18
fix: 학생 회원 목록 시안 불일치 5건 수정
시안(관리자페이지 5199:9014)을 다시 대조해 고쳤다. 1·2·5는 내가 잘못 읽은 것이고, 3·4는
@fox에 기능이 없어 추가했다(사용자 확정).

1. 이름 열의 bold를 뺐다. 시안 셀 서체가 `Pretendard:Regular`인데 `emphasis`를 넣었다.
2. 역할을 배지로 바꿨다. 시안 셀에 `type=pastel, color=primary, size=md` 배지가 들어 있는데
   평문으로 그렸다.
3. 관리 열을 없애고 행을 눌러 상세를 열게 했다. FoxTableRow에 `onSelect`,
   FoxListContainer에 `onRowSelect`를 추가했다. ``에 `role="button"`은 씌우지 않는다 —
   보조기술에서 행이 표의 일부가 아니게 되어 열 이름 읽기가 끊긴다. 초점만 받게 하고
   Enter·Space를 직접 듣는다. 모달 상태는 student-list가 갖고, 쓰임이 사라진
   student-row-actions는 지웠다.
4. 정렬 셀렉트 아이콘을 ArrowsDownUp으로 바꿨다. 시안은 정렬과 페이지 크기가 같은 select-txt를
   쓰면서 아이콘만 다른데(노드 293:138376 vs 293:139748), FoxSelectText가 캐럿을 하드코딩해
   교체할 수 없었다. `icon` prop을 넣고 기본값은 캐럿 그대로 뒀다.
5. 엑셀 버튼을 secondary + DownloadSimple 아이콘으로 바꿨다. 시안 글자색이
   `button/secondary-font`(#0b50d0)이고 20px 아이콘이 붙어 있다.

검증 — 이름 굵기 400, 역할 `fox-badge--primary --pastel --md`, 열 12개(마지막 가입일)에
행이 clickable·tabIndex 0이고 클릭 시 모달이 열린다, 정렬 아이콘이 페이지 크기 아이콘과 다르다,
엑셀 버튼이 `fox-button--secondary`에 아이콘을 갖는다.

Co-Authored-By: Claude Opus 5 
@83cd9bbfbb824948aba3c0ad7f64856c1eae1328
@fox/core/components/fox-list-container/fox-list-container.tsx
--- @fox/core/components/fox-list-container/fox-list-container.tsx
+++ @fox/core/components/fox-list-container/fox-list-container.tsx
@@ -51,6 +51,8 @@
   caption?: string;
   /** 행이 없을 때 표 안에 보여줄 말. */
   emptyMessage?: ReactNode;
+  /** 주면 행 전체가 눌린다 — 관리 열 없이 행을 눌러 상세를 여는 시안에 쓴다. */
+  onRowSelect?: (row: T, index: number) => void;
 
   // ── 도구 줄 ───────────────────────────────────────────────────────────────
   /** 주면 "총 N건"이 나온다. 없으면 그 자리를 비운다. */
@@ -109,6 +111,7 @@
   rowKey,
   caption,
   emptyMessage = "조회된 내용이 없습니다.",
+  onRowSelect,
   totalCount,
   toolbarLeading,
   sorts,
@@ -207,7 +210,12 @@
             </FoxTableRow>
           ) : (
             rows.map((row, index) => (
-              <FoxTableRow key={rowKey(row, index)}>
+              <FoxTableRow
+                key={rowKey(row, index)}
+                onSelect={
+                  onRowSelect ? () => onRowSelect(row, index) : undefined
+                }
+              >
                 {columns.map((column) => (
                   <FoxTableCell
                     key={column.key}
@fox/core/components/fox-select-text/fox-select-text.tsx
--- @fox/core/components/fox-select-text/fox-select-text.tsx
+++ @fox/core/components/fox-select-text/fox-select-text.tsx
@@ -36,6 +36,11 @@
    * ("2026년 8월")이라 무엇을 고르는 자리인지는 소리로 전달되지 않는다 — `"조회 기간"`처럼
    * 적어 준다.
    */
+  /**
+   * 오른쪽 표시를 바꾼다. 기본은 펼침을 뜻하는 캐럿이고, 시안이 다른 뜻을 담을 때만 준다
+   * (예: 정렬 셀렉트의 위아래 화살표). 장식이라 `aria-hidden`인 자리다.
+   */
+  icon?: ReactNode;
   ariaLabel?: string;
   /** 이름 역할을 하는 요소의 id. `ariaLabel`보다 우선한다. */
   labelledBy?: string;
@@ -76,6 +81,7 @@
   onValueChange,
   placeholder = "선택해주세요",
   disabled = false,
+  icon,
   ariaLabel,
   labelledBy,
   id,
@@ -219,7 +225,7 @@
       >
         <span className="fox-select-text__value">{selectedOption?.label ?? placeholder}</span>
         <span className="fox-select-text__icon" aria-hidden="true">
-          <ChevronMark />
+          {icon ?? <ChevronMark />}
         </span>
       </button>
 
@fox/core/components/fox-table-row/fox-table-row.tsx
--- @fox/core/components/fox-table-row/fox-table-row.tsx
+++ @fox/core/components/fox-table-row/fox-table-row.tsx
@@ -16,6 +16,14 @@
   head?: boolean;
   /** 참이면 고른 줄이 된다 — 바탕과 글자가 파랗게 바뀐다(시안 list 표). */
   selected?: boolean;
+  /**
+   * 주면 행 전체가 눌린다 — 시안이 관리 열 없이 행을 눌러 상세를 여는 경우다.
+   *
+   * `<tr>`에 `role="button"`을 씌우지 않는다 — 그러면 보조기술에서 행이 표의 일부가 아니게
+   * 되어 열 이름 읽기가 끊긴다. 대신 초점을 받게만 하고 Enter·Space를 직접 듣는다. 행의
+   * 이름은 셀 내용이 대신하므로 눌러야 할 행이 무엇인지 알 수 있다.
+   */
+  onSelect?: () => void;
   /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */
   hidden?: boolean;
   /** 배치 조정용. */
@@ -52,6 +60,7 @@
   children,
   head = false,
   selected = false,
+  onSelect,
   hidden = false,
   className,
 }: FoxTableRowProps) {
@@ -59,15 +68,31 @@
     return null;
   }
 
+  const clickable = Boolean(onSelect) && !head;
+
   return (
     <tr
       className={cx(
         "fox-table-row",
         head && "fox-table-row--head",
         selected && "fox-table-row--selected",
+        clickable && "fox-table-row--clickable",
         className
       )}
       aria-selected={selected || undefined}
+      tabIndex={clickable ? 0 : undefined}
+      onClick={clickable ? onSelect : undefined}
+      onKeyDown={
+        clickable
+          ? (event) => {
+              // Space는 기본 동작이 스크롤이라 막는다.
+              if (event.key === "Enter" || event.key === " ") {
+                event.preventDefault();
+                onSelect?.();
+              }
+            }
+          : undefined
+      }
     >
       {head ? markCells(children) : children}
     </tr>
@fox/styles/_fox-table-row.scss
--- @fox/styles/_fox-table-row.scss
+++ @fox/styles/_fox-table-row.scss
@@ -18,4 +18,18 @@
     background: fox.color(action-primary-hover);
     color: fox.color(font-theme-primary);
   }
+
+  // 행 전체가 눌리는 경우 — 누를 수 있다는 것이 보이고, 키보드 초점도 드러나야 한다.
+  &--clickable {
+    cursor: pointer;
+
+    &:hover {
+      background: fox.color(action-default-hover);
+    }
+
+    &:focus-visible {
+      outline: fox.border(2) solid fox.color(border-theme-primary);
+      outline-offset: calc(-1 * #{fox.border(2)});
+    }
+  }
 }
app/(protected)/(basic)/students/_components/student-list.tsx
--- app/(protected)/(basic)/students/_components/student-list.tsx
+++ app/(protected)/(basic)/students/_components/student-list.tsx
@@ -1,12 +1,18 @@
 'use client';
 
 import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { FoxBadge } from '@fox/core/components/fox-badge';
 import { FoxButton } from '@fox/core/components/fox-button';
 import {
   FoxListContainer,
   type FoxListColumn,
 } from '@fox/core/components/fox-list-container';
 import { FoxSelectText } from '@fox/core/components/fox-select-text';
+import {
+  FoxArrowsDownUpIcon,
+  FoxDownloadSimpleIcon,
+} from '@fox/core/icons';
 import type { StudentMember } from '@/lib/domain/student-member';
 import {
   STUDENT_MEMBERS_EXCEL_PATH,
@@ -19,7 +25,7 @@
   type StudentMemberSearchField,
   type StudentMemberSortOption,
 } from '@/lib/domain/student-member-query';
-import { StudentRowActions } from './student-row-actions';
+import { StudentDetailModal } from './student-detail-modal';
 
 interface StudentListProps {
   items: StudentMember[];
@@ -66,6 +72,7 @@
   totalCount,
 }: StudentListProps) {
   const router = useRouter();
+  const [detailMember, setDetailMember] = useState<StudentMember | null>(null);
 
   /** 목록 조건이 바뀌면 늘 1페이지로 되돌린다 — 이전 페이지 번호는 새 조건에서 의미가 다르다. */
   function go(patch: Partial<StudentMemberQuery>) {
@@ -81,7 +88,7 @@
       render: (_row, index) => (currentPage - 1) * query.pageSize + index + 1,
     },
     { key: 'memberCode', header: '회원코드', width: 120 },
-    { key: 'name', header: '이름', width: 120, emphasis: true },
+    { key: 'name', header: '이름', width: 120 },
     { key: 'loginId', header: '아이디', width: 160 },
     {
       key: 'phoneNumber',
@@ -90,7 +97,16 @@
       render: (row) => text(row.phoneNumber),
     },
     { key: 'email', header: '이메일', width: 200, render: (row) => text(row.email) },
-    { key: 'role', header: '역할', width: 120 },
+    {
+      key: 'role',
+      header: '역할',
+      width: 120,
+      render: (row) => (
+        <FoxBadge type="pastel" color="primary" size="md">
+          {row.role}
+        </FoxBadge>
+      ),
+    },
     {
       key: 'schoolName',
       header: '학교',
@@ -116,16 +132,11 @@
       width: 160,
       render: (row) => text(row.joinedAt),
     },
-    {
-      key: 'actions',
-      header: '관리',
-      width: 160,
-      render: (row) => <StudentRowActions member={row} />,
-    },
   ];
 
   return (
-    <FoxListContainer<StudentMember>
+    <>
+      <FoxListContainer<StudentMember>
       title="학생 회원 목록"
       breadcrumb={[
         { label: '홈', href: '/' },
@@ -143,6 +154,7 @@
           <FoxSelectText
             size="sm"
             ariaLabel="정렬"
+            icon={<FoxArrowsDownUpIcon />}
             options={STUDENT_MEMBER_SORT_OPTIONS.map((option) => ({
               value: option.value,
               label: option.label,
@@ -176,13 +188,15 @@
         // 검색·페이징 값을 싣지 않는 것이 사양이다(파일은 항상 전체 데이터).
         <form action={STUDENT_MEMBERS_EXCEL_PATH} method="get">
           <FoxButton
-            type="primary"
+            type="secondary"
             size="md"
             htmlType="submit"
+            leadingIcon={<FoxDownloadSimpleIcon />}
             label="엑셀 다운로드"
           />
         </form>
       }
+      onRowSelect={(row) => setDetailMember(row)}
       page={currentPage}
       totalPages={totalPages}
       buildHref={(patch) =>
@@ -197,6 +211,13 @@
           });
         }
       }}
-    />
+      />
+      {detailMember && (
+        <StudentDetailModal
+          member={detailMember}
+          onClose={() => setDetailMember(null)}
+        />
+      )}
+    </>
   );
 }
 
app/(protected)/(basic)/students/_components/student-row-actions.tsx (deleted)
--- app/(protected)/(basic)/students/_components/student-row-actions.tsx
@@ -1,40 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-import { FoxButton } from '@fox/core/components/fox-button';
-import type { StudentMember } from '@/lib/domain/student-member';
-import { StudentDetailModal } from './student-detail-modal';
-
-interface StudentRowActionsProps {
-  member: StudentMember;
-}
-
-/**
- * 목록 행의 "관리" 셀 — 조회 팝업의 열림 상태만 소유하는 최말단 상호작용 경계다(테이블 전체를
- * 클라이언트로 내리지 않기 위해 이 셀만 분리했다 — §2.4 "'use client'는 최말단에만"). 팝업에
- * 필요한 데이터는 이미 이 행이 갖고 있는 member를 그대로 전달한다(재조회 없음).
- *
- * 시안(관리자페이지 QsFVFFUKGH68xAPVkF3SJ9 5199:9014)의 관리 열은 "학생 회원 조회" 텍스트
- * 버튼이다 — 앞서 기획 시안을 근거로 돋보기 아이콘을 넣었으나, 이 Figma 시안이 더 최신이라
- * 그쪽으로 맞췄다(사용자 확정). 누르면 상세 팝업이 열리는 동작은 그대로다.
- */
-export function StudentRowActions({ member }: StudentRowActionsProps) {
-  const [isDetailOpen, setIsDetailOpen] = useState(false);
-
-  return (
-    <>
-      <FoxButton
-        type="default"
-        size="sm"
-        label="학생 회원 조회"
-        onAction={() => setIsDetailOpen(true)}
-      />
-      {isDetailOpen && (
-        <StudentDetailModal
-          member={member}
-          onClose={() => setIsDetailOpen(false)}
-        />
-      )}
-    </>
-  );
-}
Add a comment
List