o2o-negosium-original/negodata/front/src/features/auth/components/ProfileSheet.tsx
Mina Choi a40257ddc1 [feat] negodata: 회원관리 / 최고관리자(OWNER)
- OWNER 권한 신설, 최고관리자가 자기 회사 소속 직원(USER) 계정 생성·수정·삭제 관리
- 회사 사용자 API(/v1/company/user/*), 회원관리 페이지·폼
- 로그인/프로필 흐름 정비, 무인증 계정생성(/auth/create) 제거

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 13:31:32 +09:00

144 lines
5.5 KiB
TypeScript

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { showToast } from '@/lib/notify';
import { Typography } from '@/components/ui/typography';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Sheet } from '@/components/ui/sheet';
import { useAuth } from '../useAuth';
import { updateMe } from '../service';
const schema = z.object({
name: z.string().trim(),
email: z.string().trim().email('이메일 형식을 확인해 주십시오.').or(z.literal('')),
contactNumber: z.string().trim(),
password: z.string(),
passwordConfirm: z.string(),
});
type FormValues = z.infer<typeof schema>;
const inputClass = 'text-foreground text-xs';
const blank = (v: string) => (v.trim() ? v.trim() : null);
export function ProfileSheet({ open, onClose }: { open: boolean; onClose: () => void }) {
const { user } = useAuth();
const {
register,
handleSubmit,
setError,
formState: { errors, isSubmitting },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
name: user?.name ?? '',
email: user?.email ?? '',
contactNumber: user?.contact ?? '',
password: '',
passwordConfirm: '',
},
});
const onValid = async (v: FormValues) => {
if (v.password && v.password.length < 4) {
setError('password', { message: '비밀번호는 4자 이상으로 설정해 주십시오.' });
return;
}
if (v.password && v.password !== v.passwordConfirm) {
setError('passwordConfirm', { message: '비밀번호가 일치하지 않습니다.' });
return;
}
try {
await updateMe({
name: blank(v.name),
email: blank(v.email),
contact_number: blank(v.contactNumber),
...(v.password ? { password: v.password } : {}),
});
showToast('내 정보가 수정되었습니다.', 'success');
onClose();
} catch (err) {
showToast(err instanceof Error ? err.message : '내 정보 수정 실패', 'error');
}
};
return (
<Sheet open={open} title="내 정보 수정" onClose={onClose}>
<form onSubmit={handleSubmit(onValid)} className="mt-6 space-y-4 text-xs font-mono">
{/* 읽기 전용: 회사 / 로그인ID / 권한 */}
<div className="rounded-md border border-border bg-muted/40 p-3 space-y-1.5">
<div className="flex justify-between">
<Typography variant="caption">회사</Typography>
<Typography variant="caption" className="font-bold text-foreground">{user?.company}</Typography>
</div>
<div className="flex justify-between">
<Typography variant="caption">로그인 ID</Typography>
<Typography variant="caption" className="font-mono font-bold text-foreground">{user?.loginId}</Typography>
</div>
<div className="flex justify-between">
<Typography variant="caption">권한등급</Typography>
<Typography variant="caption" className="font-bold text-foreground">{user?.role}</Typography>
</div>
</div>
{/* 이름 */}
<div className="space-y-1">
<Typography as="label" variant="label">이름</Typography>
<Input id="profile-name" type="text" {...register('name')} className={inputClass} placeholder="이름" />
</div>
{/* 이메일 */}
<div className="space-y-1">
<Typography as="label" variant="label">이메일</Typography>
<Input id="profile-email" type="email" {...register('email')} className={inputClass} placeholder="me@company.co.kr" />
{errors.email && <p className="text-[10px] text-rose-500">{errors.email.message}</p>}
</div>
{/* 연락처 */}
<div className="space-y-1">
<Typography as="label" variant="label">연락처</Typography>
<Input id="profile-phone" type="text" {...register('contactNumber')} className={inputClass} placeholder="010-XXXX-XXXX" />
</div>
{/* 비밀번호 변경(옵션) */}
<div className="space-y-1">
<Typography as="label" variant="label">비밀번호 변경 (변경 시에만 입력)</Typography>
<Input
id="profile-password"
type="password"
{...register('password')}
className={inputClass}
placeholder="비워두면 기존 비밀번호 유지"
autoComplete="new-password"
/>
{errors.password && <p className="text-[10px] text-rose-500">{errors.password.message}</p>}
</div>
{/* 비밀번호 확인 */}
<div className="space-y-1">
<Typography as="label" variant="label">비밀번호 확인</Typography>
<Input
id="profile-password-confirm"
type="password"
{...register('passwordConfirm')}
className={inputClass}
placeholder="비밀번호를 한 번 더 입력"
autoComplete="new-password"
/>
{errors.passwordConfirm && <p className="text-[10px] text-rose-500">{errors.passwordConfirm.message}</p>}
</div>
<div className="pt-4 flex items-center gap-2 border-t border-border mt-8 justify-end">
<Button type="button" variant="outline" size="sm" onClick={onClose}>
취소
</Button>
<Button type="submit" size="sm" disabled={isSubmitting}>
변경사항 저장
</Button>
</div>
</form>
</Sheet>
);
}