[as] feat: 검토인력 인터페이스 수정 & 결재선관리 페이지 수정

This commit is contained in:
2026-06-08 16:18:42 +09:00
parent 84013993c9
commit fc70963fa9
8 changed files with 104 additions and 114 deletions
+2 -1
View File
@@ -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" },
];