File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
'use client';
import { useActionState, useEffect, useState } from 'react';
import { Button } from '@/components/ui/button';
import { Field } from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import { Modal } from '@/components/ui/modal';
import { Select } from '@/components/ui/select';
import { useFeedback } from '@/app/_hooks/use-feedback';
import {
ADMIN_LOCK_STATUS_OPTIONS,
type AdminMember,
} from '@/lib/domain/admin-member';
import {
ADMIN_PASSWORD_HELP_TEXT,
INITIAL_ADMIN_MEMBER_FORM_STATE,
} from '@/lib/domain/admin-member-form';
import { updateAdminMemberAction } from '../_actions';
import {
AdminMemberFormFields,
FieldError,
} from './admin-member-form-fields';
interface AdminMemberEditModalProps {
member: AdminMember;
onClose: () => void;
}
const FORM_ID = 'admin-member-edit-form';
/**
* 관리자 수정 팝업(시안 ADM_ADM_103_p).
*
* 이름·ID는 읽기 전용이다(시안 ①). 다만 화면의 readOnly는 표시일 뿐이라 Server Action은 두 값을
* 아예 반영하지 않는다 — 직접 POST로 다른 값을 보내도 바뀌지 않는다.
*
* **비밀번호는 [비밀번호 변경]을 눌러야 입력할 수 있다**(시안 ②). 다만 비워 둔 채로는 수정이
* 저장되지 않는다 — 백엔드 UPDATE가 비밀번호를 조건 없이 덮어써서 빈 값을 보내면 그 계정이 로그인
* 불가가 되기 때문이다(`ADMIN_PASSWORD_REQUIRED_ON_UPDATE_TEXT` 주석). 비활성 input은 FormData에
* 실리지 않으므로 버튼을 누르지 않고 제출하면 그 안내가 그대로 뜬다.
*
* 휴대전화번호·이메일은 항상 빈 칸으로 시작한다 — 백엔드가 두 값을 되돌려주지 않아 기존 값을
* 채울 수단이 없다(Repository 주석). 입력한 값으로 덮어쓰는 동작이다.
*
* "잠김여부"는 활성/비활성으로 표기되지만 백엔드 필드는 `acctLockYn`(잠김 여부)이라 의미가
* 뒤집혀 있다. 그 변환은 도메인(`ADMIN_LOCK_STATUS_OPTIONS`)이 한 곳에서 책임진다.
*/
export function AdminMemberEditModal({
member,
onClose,
}: AdminMemberEditModalProps) {
const { showToast } = useFeedback();
const [state, formAction, isPending] = useActionState(
updateAdminMemberAction,
INITIAL_ADMIN_MEMBER_FORM_STATE
);
const [isPasswordEditable, setIsPasswordEditable] = useState(false);
useEffect(() => {
if (state.status === 'success') {
showToast({ variant: 'success', message: '관리자 정보를 수정했습니다.' });
onClose();
}
}, [state, showToast, onClose]);
const errors = state.status === 'error' ? (state.errors ?? {}) : {};
return (
<Modal
title="관리자 수정"
onClose={onClose}
footer={
<>
<Button type="button" variant="ghost" onClick={onClose}>
취소
</Button>
<Button
type="submit"
form={FORM_ID}
variant="primary"
disabled={isPending}
>
{isPending ? '수정 중...' : '수정'}
</Button>
</>
}
>
<form id={FORM_ID} action={formAction} className="flex flex-col gap-4">
{/* 수정 대상을 가리키는 유일한 입력. 이름·ID는 읽기 전용이라 아예 제출하지 않는다 —
Server Action도 그 둘을 읽지 않으므로 위조해 보내도 반영되지 않는다. */}
<input type="hidden" name="id" value={member.id} />
<p className="text-right text-body-sm text-danger">
* 는 필수 항목입니다.
</p>
<Field label="이름">
<Input type="text" value={member.name} readOnly />
</Field>
<Field label="ID">
<Input type="text" value={member.loginId} readOnly />
</Field>
<Field label="비밀번호">
<div className="flex items-center gap-2">
<Input
type="password"
name="password"
placeholder="비밀번호를 입력하세요."
autoComplete="new-password"
disabled={!isPasswordEditable}
/>
<Button
type="button"
variant="secondary"
onClick={() => setIsPasswordEditable(true)}
disabled={isPasswordEditable}
className="shrink-0"
>
비밀번호 변경
</Button>
</div>
</Field>
{isPasswordEditable && (
<p className="text-body-sm text-foreground-muted">
{ADMIN_PASSWORD_HELP_TEXT}
</p>
)}
<FieldError message={errors.password} />
<AdminMemberFormFields member={member} errors={errors} />
<Field label="잠김여부">
<Select
name="isLocked"
defaultValue={String(member.isLocked ?? false)}
>
{ADMIN_LOCK_STATUS_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Select>
</Field>
{state.status === 'error' && state.message && (
<p className="text-body-sm text-danger">{state.message}</p>
)}
</form>
</Modal>
);
}