[as] feat: 검토인력 인터페이스 수정 & 결재선관리 페이지 수정
This commit is contained in:
@@ -12,10 +12,11 @@ import { rvwMnpwMngService } from "./rvw-mnpw-service";
|
||||
|
||||
const rvwMnpwMngBaseKey = [ENDPOINT.AS_COMMON_SERVICE.RVW_MNPW] as const;
|
||||
|
||||
export function useRvwMnpwMngPage(params: RvwMnpwMngSearchDto) {
|
||||
export function useRvwMnpwMngPage(params: RvwMnpwMngSearchDto, options?: { enabled?: boolean }) {
|
||||
return useQuery({
|
||||
queryKey: [...rvwMnpwMngBaseKey, "findPage", params],
|
||||
queryFn: () => rvwMnpwMngService.findPage(params),
|
||||
enabled: options?.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ export interface RvwMnpwMngResponseDto {
|
||||
ognzSn: number;
|
||||
userUuid: string;
|
||||
userUuidNm?: string;
|
||||
ognzNm?: string;
|
||||
jbpsSeNm?: string;
|
||||
jbgdSeNm?: string;
|
||||
rgtr?: string;
|
||||
regDt?: string;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import type { ElaprAplnRow } from "../model/apln-row-types";
|
||||
|
||||
export const ATRZ_RECEIVER_CD = "ATRZ00";
|
||||
export const ATRZ_REVIEWER_1_CD = "ATRZ01";
|
||||
export const ATRZ_REVIEWER_2_CD = "ATRZ02";
|
||||
export const ATRZ_APPROVER_CD = "ATRZ03";
|
||||
export const ATRZ_FINAL_CD = "ATRZ99";
|
||||
const ATRZ_RECEIVER_CD = "ATRZ01";
|
||||
const ATRZ_FINAL_CD = "ATRZ99";
|
||||
|
||||
export function getVisibleRows(rows: ElaprAplnRow[]): ElaprAplnRow[] {
|
||||
return rows.filter((row) => !row.isDeleted);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SelectedOgnzUser } from "@/widgets/ognz-user-tree";
|
||||
import type { RvwMnpwMngResponseDto } from "@/entities/rvw-mnpw";
|
||||
|
||||
export interface ReviewerOption {
|
||||
uuid: string;
|
||||
@@ -6,10 +6,10 @@ export interface ReviewerOption {
|
||||
deptNm: string;
|
||||
}
|
||||
|
||||
export function fromSelectedOgnzUser(user: SelectedOgnzUser): ReviewerOption {
|
||||
export function fromRvwMnpwMng(item: RvwMnpwMngResponseDto): ReviewerOption {
|
||||
return {
|
||||
uuid: user.userUuid,
|
||||
name: user.userNm,
|
||||
deptNm: user.ognzNm,
|
||||
uuid: item.userUuid,
|
||||
name: item.userUuidNm ?? "",
|
||||
deptNm: item.ognzNm ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { useOgnzUserTree } from "@/entities/user-organization";
|
||||
import {
|
||||
collectExpandedOgnzSnIds,
|
||||
filterOgnzUserTree,
|
||||
OgnzUserTree,
|
||||
type SelectedOgnzUser,
|
||||
} from "@/widgets/ognz-user-tree";
|
||||
import { useRvwMnpwMngPage } from "@/entities/rvw-mnpw";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
@@ -16,113 +8,109 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
Skeleton,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
import { fromSelectedOgnzUser, type ReviewerOption } from "../model/reviewer-option";
|
||||
import { fromRvwMnpwMng, type ReviewerOption } from "../model/reviewer-option";
|
||||
|
||||
const PAGE_SIZE = 500;
|
||||
|
||||
interface AprvrPickerDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
atrzSeComCd: string;
|
||||
onSelect: (reviewer: ReviewerOption) => void;
|
||||
}
|
||||
|
||||
function AprvrPickerDialog({ open, onOpenChange, onSelect }: AprvrPickerDialogProps) {
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [displayTree, setDisplayTree] = useState<ReturnType<typeof filterOgnzUserTree>>([]);
|
||||
const [expandedOgnzSnIds, setExpandedOgnzSnIds] = useState<Set<number>>(new Set());
|
||||
const [selectedUserId, setSelectedUserId] = useState<string>();
|
||||
const [isSearchActive, setIsSearchActive] = useState(false);
|
||||
function AprvrPickerDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
atrzSeComCd,
|
||||
onSelect,
|
||||
}: AprvrPickerDialogProps) {
|
||||
const { data, isPending, isError } = useRvwMnpwMngPage(
|
||||
{
|
||||
atrzSeComCd,
|
||||
page: 0,
|
||||
size: PAGE_SIZE,
|
||||
sort: "userUuidNm,asc",
|
||||
},
|
||||
{ enabled: open },
|
||||
);
|
||||
|
||||
const { data, isPending, isError } = useOgnzUserTree({ enabled: open });
|
||||
const items = data?.content ?? [];
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
setKeyword("");
|
||||
setSelectedUserId(undefined);
|
||||
setExpandedOgnzSnIds(new Set());
|
||||
setIsSearchActive(false);
|
||||
setDisplayTree(data ?? []);
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setKeyword("");
|
||||
setSelectedUserId(undefined);
|
||||
setExpandedOgnzSnIds(new Set());
|
||||
setIsSearchActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setDisplayTree(data ?? []);
|
||||
}, [open, data]);
|
||||
|
||||
const handleToggle = (ognzSn: number, isExpanded: boolean) => {
|
||||
setExpandedOgnzSnIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (isExpanded) next.add(ognzSn);
|
||||
else next.delete(ognzSn);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const filtered = filterOgnzUserTree(data ?? [], keyword);
|
||||
setDisplayTree(filtered);
|
||||
setExpandedOgnzSnIds(collectExpandedOgnzSnIds(filtered));
|
||||
setIsSearchActive(keyword.trim().length > 0);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
resetState();
|
||||
};
|
||||
|
||||
const handleSelect = (user: SelectedOgnzUser) => {
|
||||
setSelectedUserId(user.userUuid);
|
||||
onSelect(fromSelectedOgnzUser(user));
|
||||
const handleSelect = (item: (typeof items)[number]) => {
|
||||
onSelect(fromRvwMnpwMng(item));
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[30rem]" aria-describedby={undefined}>
|
||||
<DialogContent className="max-w-[56rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>검토인력 선택</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="card card-border !m-0 w-full">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2">
|
||||
<Input
|
||||
className="h-[2.5rem] !text-[1.3rem]"
|
||||
placeholder="성명·부서 검색"
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
/>
|
||||
<Button type="submit" size="sm">
|
||||
조회
|
||||
</Button>
|
||||
<Button type="button" size="sm" color="white" onClick={handleReset}>
|
||||
초기화
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="h-[36rem] overflow-auto">
|
||||
<OgnzUserTree
|
||||
nodes={displayTree}
|
||||
isPending={isPending}
|
||||
isError={isError}
|
||||
isSearchActive={isSearchActive}
|
||||
expandedOgnzSnIds={expandedOgnzSnIds}
|
||||
selectedUserId={selectedUserId}
|
||||
showSelectButton
|
||||
onToggle={handleToggle}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-[36rem] overflow-auto">
|
||||
{isPending ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-[4rem]" />
|
||||
<Skeleton className="h-[16rem]" />
|
||||
</div>
|
||||
) : isError ? (
|
||||
<p className="py-8 text-center text-lg text-muted-foreground">
|
||||
검토인력 목록을 불러오지 못했습니다.
|
||||
</p>
|
||||
) : items.length === 0 ? (
|
||||
<p className="py-8 text-center text-lg text-muted-foreground">
|
||||
배치된 검토인력이 없습니다.
|
||||
</p>
|
||||
) : (
|
||||
<Table className="w-full">
|
||||
<colgroup>
|
||||
<col width="25%" />
|
||||
<col width="25%" />
|
||||
<col width="15%" />
|
||||
<col width="15%" />
|
||||
<col width="20%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>성명</TableHead>
|
||||
<TableHead>부서</TableHead>
|
||||
<TableHead>직위</TableHead>
|
||||
<TableHead>직급</TableHead>
|
||||
<TableHead>선택</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.map((item) => (
|
||||
<TableRow
|
||||
key={`${item.atrzSeComCd}-${item.ognzSn}-${item.userUuid}`}
|
||||
>
|
||||
<TableCell align="center">
|
||||
{item.userUuidNm || item.userUuid}
|
||||
</TableCell>
|
||||
<TableCell align="center">{item.ognzNm || "-"}</TableCell>
|
||||
<TableCell align="center">{item.jbpsSeNm || "-"}</TableCell>
|
||||
<TableCell align="center">{item.jbgdSeNm || "-"}</TableCell>
|
||||
<TableCell align="center">
|
||||
<Button type="button" size="sm" onClick={() => handleSelect(item)}>
|
||||
선택
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
|
||||
@@ -75,6 +75,7 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
|
||||
const isDisabled = !elaprTmpltId;
|
||||
const serverRows = aplnQuery.data?.content ?? [];
|
||||
const canSave = hasUnsavedChanges(rows, serverRows);
|
||||
const pickingRow = pickingRowKey ? rows.find((r) => r.rowKey === pickingRowKey) : undefined;
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
@@ -236,6 +237,7 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
|
||||
<AprvrPickerDialog
|
||||
open={pickerOpen}
|
||||
onOpenChange={setPickerOpen}
|
||||
atrzSeComCd={pickingRow?.atrzSeComCd ?? ""}
|
||||
onSelect={handleReviewerSelect}
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -36,7 +36,7 @@ function SortableAplnRow({
|
||||
onChange,
|
||||
onPickAprvr,
|
||||
}: SortableAplnRowProps) {
|
||||
const { data: atrzSeCodes } = useCodeList("ATRZSECD");
|
||||
const { data: atrzSeCodes } = useCodeList("ATRZSE");
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: row.rowKey,
|
||||
@@ -50,7 +50,7 @@ function SortableAplnRow({
|
||||
};
|
||||
|
||||
const rowDisabled = isLocked && !isEditing;
|
||||
const atrzSeLabel = row.atrzSeComNm ?? findCodeLabel(atrzSeCodes, row.atrzSeComCd);
|
||||
const atrzSeLabel = row.atrzSeComNm || findCodeLabel(atrzSeCodes, row.atrzSeComCd);
|
||||
|
||||
return (
|
||||
<TableRow ref={setNodeRef} style={style} className={isEditing ? "bg-accent/30" : undefined}>
|
||||
@@ -72,7 +72,7 @@ function SortableAplnRow({
|
||||
<TableCell align="center">
|
||||
{isEditing ? (
|
||||
<CodeSelect
|
||||
upCd="ATRZSECD"
|
||||
upCd="ATRZSE"
|
||||
className="min-w-[120px]"
|
||||
value={row.atrzSeComCd ?? ""}
|
||||
onValueChange={(v) =>
|
||||
|
||||
@@ -4,11 +4,10 @@ export interface RvwMnpwBlockConfig {
|
||||
atrzSeComCd: string;
|
||||
}
|
||||
|
||||
/** 결재선 관리 validators.ts ATRZ 코드와 동일. ATRZSE / ATRZSECD 배포 환경 코드표 최종 확인 필요 */
|
||||
/** 결재선 관리 validators.ts ATRZ 코드와 동일 */
|
||||
export const RVW_MNPW_BLOCKS: RvwMnpwBlockConfig[] = [
|
||||
{ id: "receiver", title: "접수", atrzSeComCd: "ATRZ00" },
|
||||
// TBD: ATRZ02(검토2) 별도 블록 필요 여부 — 현재 ATRZ01만 검토 블록으로 사용
|
||||
{ id: "reviewer", title: "검토", atrzSeComCd: "ATRZ01" },
|
||||
{ id: "receiver", title: "접수", atrzSeComCd: "ATRZ01" },
|
||||
{ id: "reviewer", title: "검토", atrzSeComCd: "ATRZ02" },
|
||||
{ id: "approver", title: "승인", atrzSeComCd: "ATRZ03" },
|
||||
{ id: "final", title: "최종승인", atrzSeComCd: "ATRZ99" },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user