o2o-negosium-original/negodata/front/src/pages/members.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

97 lines
3.1 KiB
TypeScript

import { Plus } from 'lucide-react';
import { useOverlayRouter } from '@/lib/useOverlayRouter';
import { showToast } from '@/lib/notify';
import { confirm } from '@/lib/confirm';
import { PageContainer } from '@/components/layout/PageContainer';
import { PageToolbar, SearchInput } from '@/components/layout/PageToolbar';
import { Button } from '@/components/ui/button';
import { useServerList } from '@/lib/useServerList';
import { useMembers } from '@/features/members/hooks/useMembers';
import { MemberTable } from '@/features/members/components/MemberTable';
import { MemberFormSheet } from '@/features/members/components/MemberFormSheet';
import type { Member } from '@/features/members/types';
import type { ListCompanyUsersParams } from '@/features/members/api';
export default function MembersPage() {
const list = useServerList({ pageSize: 10 });
const params: ListCompanyUsersParams = {
search: list.debouncedSearch || undefined,
page: list.page,
size: list.pageSize,
};
const { members, total, createMember, updateMember, deleteMember } = useMembers(params);
const totalPages = list.totalPages(total);
const overlay = useOverlayRouter(['new', 'detail']);
const editId = overlay.get('detail');
const editing = editId ? members.find((m) => m.user_id === editId) ?? null : null;
const formMode: 'create' | 'edit' = editId ? 'edit' : 'create';
const isFormOpen = overlay.has('new') || !!editing;
const openCreate = () => overlay.open('new');
const openEdit = (member: Member) => overlay.open('detail', member.user_id);
const handleDelete = async (userId: string, label: string) => {
if (
await confirm({
title: '계정 삭제',
description: `[${label}] 계정을 삭제하시겠습니까?`,
confirmText: '삭제',
destructive: true,
})
) {
try {
await deleteMember(userId);
showToast('계정이 삭제되었습니다.', 'info');
} catch (err) {
showToast(err instanceof Error ? err.message : '계정 삭제 실패', 'error');
}
}
};
return (
<PageContainer>
<PageToolbar
actions={
<Button onClick={openCreate}>
<Plus />
신규 계정 생성
</Button>
}
>
<SearchInput
id="member-search"
value={list.search}
onChange={(e) => list.setSearch(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && list.submitSearch()}
placeholder="로그인 ID, 이름 또는 이메일로 검색..."
/>
</PageToolbar>
<MemberTable
data={members}
onRowClick={openEdit}
page={list.page}
totalPages={totalPages}
totalCount={total}
pageSize={list.pageSize}
onPageChange={list.setPage}
/>
{isFormOpen && (
<MemberFormSheet
key={`${formMode}-${editing?.user_id ?? 'new'}`}
open
mode={formMode}
member={editing}
onCreate={createMember}
onUpdate={updateMember}
onDelete={handleDelete}
onClose={overlay.close}
/>
)}
</PageContainer>
);
}