임동욱 임동욱 08-18
fix: 사용여부 토글이 눌리지 않던 문제 — 화면 상태로 동작하게
토글은 백엔드에 사용여부 값도 변경 API도 없어 `disabled`로 막아 두었었다. 화면에서는 움직이게
해 달라는 결정에 따라 잠금을 풀었다(사용자 확정).

- 토글 상태를 이 컴포넌트가 갖는다. 종전에는 라벨을 prop에서 계산해(`member.isActive ? …`)
  잠금을 풀어도 스위치만 움직이고 글자는 그대로였을 것이다 — 함께 고쳤다.
- 저장 버튼의 잠금도 풀었다. 다만 Server Action은 여전히 미지원을 알리므로, 그 사유를
  모달 foot 왼쪽(utility 슬롯)에 `role="alert"`로 띄운다. 종전에는 title 속성에만 있어
  눈에 띄지 않았다.
- 성공 시 닫는 useEffect를 지웠다. 지금 이 액션은 성공을 돌려주지 않는다 — 변경 API가 생겨
  성공 경로가 살아나면 그때 함께 되살린다.

한계 — 토글 값은 화면에만 머문다. 저장되지 않고 목록에도 반영되지 않으며 팝업을 닫으면
사라진다. 실제 저장이 필요하면 백엔드 API가 생기거나 mock 오버레이(lib/data/mock)를 두어야
한다.

검증 — 비활성↔활성으로 체크와 라벨이 함께 바뀌고(트랙 #256ef4), 저장 버튼이 활성이다.
오류 문구는 세션이 필요한 Server Action 응답이라 프리뷰에서는 경로만 확인했다.

Co-Authored-By: Claude Opus 5 
@30728468cbe0f935762117650e6a481b0cc54c1a
app/(protected)/(basic)/students/_components/student-detail-modal.module.scss
--- app/(protected)/(basic)/students/_components/student-detail-modal.module.scss
+++ app/(protected)/(basic)/students/_components/student-detail-modal.module.scss
@@ -1,3 +1,5 @@
+@use "@fox/styles/abstracts" as fox;
+
 // 학생 회원 조회 팝업 — 이 화면에만 필요한 배치.
 
 // FoxModal의 `contents`가 `align-items: flex-start`인 flex 열이라, 그 안의 블록 요소는
@@ -5,3 +7,13 @@
 .form {
   inline-size: 100%;
 }
+
+// 저장 실패 사유. 사용여부 변경 API가 아직 없어 저장을 누르면 이 자리에 뜬다.
+.error {
+  margin: 0;
+  color: fox.color(font-system-danger);
+  font-family: fox.font-family(body);
+  font-size: fox.font-size(body-sm);
+  line-height: 1.5;
+  letter-spacing: -0.025em;
+}
app/(protected)/(basic)/students/_components/student-detail-modal.tsx
--- app/(protected)/(basic)/students/_components/student-detail-modal.tsx
+++ app/(protected)/(basic)/students/_components/student-detail-modal.tsx
@@ -1,6 +1,6 @@
 'use client';
 
-import { useActionState, useEffect, useRef } from 'react';
+import { useActionState, useRef, useState } from 'react';
 import { FoxButton } from '@fox/core/components/fox-button';
 import {
   FoxDescriptionList,
@@ -26,15 +26,6 @@
 
 const INITIAL_STATE: UpdateStudentActiveStatusState = { status: 'idle' };
 const DETAIL_FORM_ID = 'student-active-status-form';
-
-/**
- * 사용여부 수정 가능 여부의 단일 토글. 백엔드에 사용여부 값도 변경 API도 없어 현재는 false다 —
- * 변경 API가 생기면 이 상수를 true로 되돌리고 `_actions.ts`의 미지원 처리를 실제 구현으로
- * 바꾸면 된다(토글·저장 버튼의 비활성이 함께 풀린다).
- */
-const IS_ACTIVE_STATUS_EDITABLE = false;
-const ACTIVE_STATUS_UNSUPPORTED_NOTE =
-  '사용여부 변경은 백엔드 API 준비 후 제공됩니다.';
 
 /** 시안은 두 값을 " / "로 묶는다("홍라애 / 010-2222-1234"). 둘 다 없으면 `-`다. */
 function joinPair(first: string | null, second: string | null) {
@@ -67,12 +58,9 @@
     INITIAL_STATE
   );
   const formRef = useRef<HTMLFormElement>(null);
-
-  useEffect(() => {
-    if (state.status === 'success') {
-      onClose();
-    }
-  }, [state, onClose]);
+  // 토글은 화면에서 바로 반응해야 하므로 이 컴포넌트가 상태를 갖는다 — prop에서 계산하면
+  // 스위치를 넘겨도 라벨이 그대로다. 저장이 되지 않으므로 이 값은 화면에만 머문다.
+  const [isActive, setIsActive] = useState(member.isActive ?? false);
 
   const items: FoxDescriptionItem[] = [
     { key: 'name', term: '이름', description: member.name },
@@ -114,9 +102,9 @@
         <FoxToggleSwitch
           name="isActive"
           value="true"
-          label={member.isActive ? '활성' : '비활성'}
-          defaultChecked={member.isActive ?? false}
-          disabled={!IS_ACTIVE_STATUS_EDITABLE}
+          label={isActive ? '활성' : '비활성'}
+          checked={isActive}
+          onChange={(checked) => setIsActive(checked)}
         />
       ),
     },
@@ -128,6 +116,13 @@
       size="sm"
       title="학생 회원 조회"
       onClose={onClose}
+      utility={
+        state.status === 'error' ? (
+          <p className={styles.error} role="alert">
+            {state.error}
+          </p>
+        ) : undefined
+      }
       actions={
         <>
           <FoxButton
@@ -137,21 +132,14 @@
             onAction={onClose}
           />
           {/* FoxButton에는 폼을 가리키는 prop이 없다(네이티브 `form` 속성 미노출). 버튼이
-              foot 슬롯에 그려져 폼의 자손이 아니므로 ref로 직접 제출한다.
-              비활성 사유는 감싼 span의 title로 남긴다 — 비활성 버튼은 hover를 받지 못한다. */}
-          <span
-            title={
-              IS_ACTIVE_STATUS_EDITABLE ? undefined : ACTIVE_STATUS_UNSUPPORTED_NOTE
-            }
-          >
-            <FoxButton
-              type="primary"
-              size="md"
-              label={isPending ? '저장 중...' : '저장'}
-              disabled={isPending || !IS_ACTIVE_STATUS_EDITABLE}
-              onAction={() => formRef.current?.requestSubmit()}
-            />
-          </span>
+              foot 슬롯에 그려져 폼의 자손이 아니므로 ref로 직접 제출한다. */}
+          <FoxButton
+            type="primary"
+            size="md"
+            label={isPending ? '저장 중...' : '저장'}
+            disabled={isPending}
+            onAction={() => formRef.current?.requestSubmit()}
+          />
         </>
       }
     >
Add a comment
List