[as] feat: 검토인력관리 페이지 구현
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
export { collectExpandedOgnzSnIds, filterOgnzUserTree } from "./lib/filter-ognz-user-tree";
|
||||
export { type SelectedOgnzUser, toSelectedOgnzUser } from "./model/selected-ognz-user";
|
||||
export { OgnzUserTree } from "./ui/ognz-user-tree";
|
||||
export { OgnzUserTreeNode } from "./ui/ognz-user-tree-node";
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { OgnzUserTreeResponseDto } from "@/entities/user-organization";
|
||||
|
||||
export function filterOgnzUserTree(
|
||||
nodes: OgnzUserTreeResponseDto[],
|
||||
keyword: string,
|
||||
): OgnzUserTreeResponseDto[] {
|
||||
const q = keyword.trim().toLowerCase();
|
||||
if (!q) return nodes;
|
||||
|
||||
return nodes
|
||||
.map((node) => {
|
||||
const ognzMatches = node.ognzNm.toLowerCase().includes(q);
|
||||
const filteredUsers = ognzMatches
|
||||
? node.users
|
||||
: node.users.filter((user) => user.userNm.toLowerCase().includes(q));
|
||||
const filteredChildren = filterOgnzUserTree(node.children ?? [], keyword);
|
||||
|
||||
if (ognzMatches || filteredUsers.length > 0 || filteredChildren.length > 0) {
|
||||
return {
|
||||
...node,
|
||||
users: filteredUsers,
|
||||
children: filteredChildren,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((node): node is OgnzUserTreeResponseDto => node !== null);
|
||||
}
|
||||
|
||||
export function collectExpandedOgnzSnIds(nodes: OgnzUserTreeResponseDto[]): Set<number> {
|
||||
const ids = new Set<number>();
|
||||
|
||||
function walk(list: OgnzUserTreeResponseDto[]) {
|
||||
for (const node of list) {
|
||||
const hasContent = (node.users?.length ?? 0) > 0 || (node.children?.length ?? 0) > 0;
|
||||
if (hasContent) ids.add(node.ognzSn);
|
||||
walk(node.children ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
walk(nodes);
|
||||
return ids;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { OgnzUserTreeResponseDto, OgnzUserTreeUserDto } from "@/entities/user-organization";
|
||||
|
||||
export interface SelectedOgnzUser {
|
||||
userUuid: string;
|
||||
userNm: string;
|
||||
ognzSn: number;
|
||||
ognzNm: string;
|
||||
jbgdSeCd?: string;
|
||||
jbpsSeCd?: string;
|
||||
}
|
||||
|
||||
export function toSelectedOgnzUser(
|
||||
user: OgnzUserTreeUserDto,
|
||||
node: Pick<OgnzUserTreeResponseDto, "ognzSn" | "ognzNm">,
|
||||
): SelectedOgnzUser {
|
||||
return {
|
||||
userUuid: user.userUnqId,
|
||||
userNm: user.userNm,
|
||||
ognzSn: node.ognzSn,
|
||||
ognzNm: node.ognzNm,
|
||||
jbgdSeCd: user.jbgdSeCd || undefined,
|
||||
jbpsSeCd: user.jbpsSeCd || undefined,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronRight } from "lucide-react";
|
||||
|
||||
import type { OgnzUserTreeResponseDto } from "@/entities/user-organization";
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@common/components";
|
||||
|
||||
import { type SelectedOgnzUser, toSelectedOgnzUser } from "../model/selected-ognz-user";
|
||||
|
||||
interface OgnzUserTreeNodeProps {
|
||||
node: OgnzUserTreeResponseDto;
|
||||
depth?: number;
|
||||
expandedOgnzSnIds: Set<number>;
|
||||
selectedUserId?: string;
|
||||
showSelectButton?: boolean;
|
||||
onToggle: (ognzSn: number, open: boolean) => void;
|
||||
onSelect: (user: SelectedOgnzUser) => void;
|
||||
}
|
||||
|
||||
function OgnzUserTreeNode({
|
||||
node,
|
||||
depth = 0,
|
||||
expandedOgnzSnIds,
|
||||
selectedUserId,
|
||||
showSelectButton = false,
|
||||
onToggle,
|
||||
onSelect,
|
||||
}: OgnzUserTreeNodeProps) {
|
||||
const hasUsers = (node.users?.length ?? 0) > 0;
|
||||
const hasChildren = (node.children?.length ?? 0) > 0;
|
||||
const isExpandable = hasUsers || hasChildren;
|
||||
const isOpen = expandedOgnzSnIds.has(node.ognzSn);
|
||||
const indent = depth * 16 + 8;
|
||||
|
||||
if (!isExpandable) {
|
||||
return (
|
||||
<div style={{ paddingInlineStart: indent }} className="py-1">
|
||||
<span className="text-[1.4rem] font-medium text-label">{node.ognzNm}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => onToggle(node.ognzSn, open)}
|
||||
className="w-full"
|
||||
>
|
||||
<div style={{ paddingInlineStart: indent }}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="group flex w-full items-center gap-2 rounded-sm p-2 text-left hover:bg-accent"
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
"size-5 shrink-0 text-label transition-transform",
|
||||
isOpen && "rotate-90",
|
||||
)}
|
||||
/>
|
||||
<span className="text-[1.4rem] font-medium text-label">{node.ognzNm}</span>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
</div>
|
||||
|
||||
<CollapsibleContent>
|
||||
{node.users.map((user) => {
|
||||
const isSelected = selectedUserId === user.userUnqId;
|
||||
const selectedUser = toSelectedOgnzUser(user, node);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={user.userUnqId}
|
||||
style={{ paddingInlineStart: indent + 24 }}
|
||||
className={cn(
|
||||
"group flex cursor-pointer items-center justify-between gap-2 rounded-sm p-2 hover:bg-accent",
|
||||
isSelected && "!border !border-primary !bg-primary-transparent",
|
||||
)}
|
||||
onClick={() => onSelect(selectedUser)}
|
||||
>
|
||||
<span className="text-[1.4rem] text-label">{user.userNm}</span>
|
||||
{showSelectButton ? (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelect(selectedUser);
|
||||
}}
|
||||
>
|
||||
선택
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{node.children.map((child) => (
|
||||
<OgnzUserTreeNode
|
||||
key={child.ognzSn}
|
||||
node={child}
|
||||
depth={depth + 1}
|
||||
expandedOgnzSnIds={expandedOgnzSnIds}
|
||||
selectedUserId={selectedUserId}
|
||||
showSelectButton={showSelectButton}
|
||||
onToggle={onToggle}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
export { OgnzUserTreeNode };
|
||||
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import type { OgnzUserTreeResponseDto } from "@/entities/user-organization";
|
||||
import { Skeleton } from "@common/components";
|
||||
|
||||
import type { SelectedOgnzUser } from "../model/selected-ognz-user";
|
||||
|
||||
import { OgnzUserTreeNode } from "./ognz-user-tree-node";
|
||||
|
||||
interface OgnzUserTreeProps {
|
||||
nodes: OgnzUserTreeResponseDto[];
|
||||
isPending?: boolean;
|
||||
isError?: boolean;
|
||||
isSearchActive?: boolean;
|
||||
expandedOgnzSnIds: Set<number>;
|
||||
selectedUserId?: string;
|
||||
showSelectButton?: boolean;
|
||||
onToggle: (ognzSn: number, open: boolean) => void;
|
||||
onSelect: (user: SelectedOgnzUser) => void;
|
||||
}
|
||||
|
||||
function OgnzUserTree({
|
||||
nodes,
|
||||
isPending,
|
||||
isError,
|
||||
isSearchActive = false,
|
||||
expandedOgnzSnIds,
|
||||
selectedUserId,
|
||||
showSelectButton = false,
|
||||
onToggle,
|
||||
onSelect,
|
||||
}: OgnzUserTreeProps) {
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-10 w-3/4" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<p className="py-8 text-center text-lg text-muted-foreground">
|
||||
조직 사용자 목록을 불러오지 못했습니다.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
if (nodes.length === 0) {
|
||||
return (
|
||||
<p className="py-8 text-center text-lg text-muted-foreground">
|
||||
{isSearchActive ? "조회된 인원이 없습니다." : "등록된 조직/사용자가 없습니다."}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{nodes.map((node) => (
|
||||
<OgnzUserTreeNode
|
||||
key={node.ognzSn}
|
||||
node={node}
|
||||
expandedOgnzSnIds={expandedOgnzSnIds}
|
||||
selectedUserId={selectedUserId}
|
||||
showSelectButton={showSelectButton}
|
||||
onToggle={onToggle}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { OgnzUserTree };
|
||||
Reference in New Issue
Block a user