- OWNER 권한 신설, 최고관리자가 자기 회사 소속 직원(USER) 계정 생성·수정·삭제 관리 - 회사 사용자 API(/v1/company/user/*), 회원관리 페이지·폼 - 로그인/프로필 흐름 정비, 무인증 계정생성(/auth/create) 제거 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
144 lines
5.5 KiB
TypeScript
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>
|
|
);
|
|
}
|