Merge remote-tracking branch 'origin/iipbs/as/dev' into iipbs/as/dev

This commit is contained in:
2026-05-21 13:13:36 +09:00
31 changed files with 1093 additions and 166 deletions
@@ -0,0 +1,70 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
import type {
CklstArtcl,
CklstArtclId,
CklstArtclSaveRequest,
CklstArtclSearchParams,
CklstArtclService,
CklstArtclUpdateRequest,
} from "../model/cklst-artcl-interface";
async function findPage(params: CklstArtclSearchParams): Promise<PageResponse<CklstArtcl>> {
const response = await client.get<PageResponse<CklstArtcl>>(
ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL,
{ params },
);
return response.data;
}
async function findByCklstSn(cklstSn: number): Promise<CklstArtcl[]> {
const response = await client.get<CklstArtcl[]>(
`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL}/by-cklst/${cklstSn}`,
);
return response.data;
}
async function findById(cklstArtclSn: number, cklstSn: number): Promise<CklstArtcl> {
const response = await client.get<CklstArtcl>(
`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL}/${cklstArtclSn}/${cklstSn}`,
);
return response.data;
}
async function create(body: CklstArtclSaveRequest): Promise<CklstArtclId> {
const response = await client.post<CklstArtclId>(
ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL,
body,
);
return response.data;
}
async function update(
cklstArtclSn: number,
cklstSn: number,
body: CklstArtclUpdateRequest,
): Promise<CklstArtclId> {
const response = await client.put<CklstArtclId>(
`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL}/${cklstArtclSn}/${cklstSn}`,
body,
);
return response.data;
}
async function remove(cklstArtclSn: number, cklstSn: number): Promise<CklstArtclId> {
const response = await client.delete<CklstArtclId>(
`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL}/${cklstArtclSn}/${cklstSn}`,
);
return response.data;
}
export const cklstArtclService = {
findPage,
findByCklstSn,
findById,
create,
update,
remove,
} satisfies CklstArtclService;
@@ -0,0 +1,48 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
import type {
CklstMng,
CklstMngSaveRequest,
CklstMngSearchParams,
CklstMngService,
} from "../model/cklst-mng-interface";
async function findPage(params: CklstMngSearchParams): Promise<PageResponse<CklstMng>> {
const response = await client.get<PageResponse<CklstMng>>(ENDPOINT.AS_CHECKLIST_SERVICE.CKLST, {
params,
});
return response.data;
}
async function findById(cklstSn: number): Promise<CklstMng> {
const response = await client.get<CklstMng>(`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST}/${cklstSn}`);
return response.data;
}
async function create(body: CklstMngSaveRequest): Promise<number> {
const response = await client.post<number>(ENDPOINT.AS_CHECKLIST_SERVICE.CKLST, body);
return response.data;
}
async function update(cklstSn: number, body: CklstMngSaveRequest): Promise<number> {
const response = await client.put<number>(
`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST}/${cklstSn}`,
body,
);
return response.data;
}
async function remove(cklstSn: number): Promise<number> {
const response = await client.delete<number>(`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST}/${cklstSn}`);
return response.data;
}
export const cklstMngService = {
findPage,
findById,
create,
update,
remove,
} satisfies CklstMngService;
+167
View File
@@ -0,0 +1,167 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import type {
CklstArtclSaveRequest,
CklstArtclSearchParams,
CklstArtclUpdateRequest,
} from "../model/cklst-artcl-interface";
import type { CklstMngSaveRequest, CklstMngSearchParams } from "../model/cklst-mng-interface";
import { cklstArtclService } from "./cklst-artcl-service";
import { cklstMngService } from "./cklst-mng-service";
const cklstMngBaseKey = [ENDPOINT.AS_CHECKLIST_SERVICE.CKLST] as const;
const cklstArtclBaseKey = [ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL] as const;
export function useCklstMngPage(params: CklstMngSearchParams) {
return useQuery({
queryKey: [...cklstMngBaseKey, "page", params],
queryFn: () => cklstMngService.findPage(params),
});
}
export function useCklstMng(cklstSn: number | null | undefined) {
return useQuery({
queryKey: [...cklstMngBaseKey, cklstSn],
queryFn: () => cklstMngService.findById(cklstSn!),
enabled: cklstSn != null,
});
}
export function useCreateCklstMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (body: CklstMngSaveRequest) => cklstMngService.create(body),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: cklstMngBaseKey });
},
});
}
export function useUpdateCklstMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ cklstSn, body }: { cklstSn: number; body: CklstMngSaveRequest }) =>
cklstMngService.update(cklstSn, body),
onSuccess: (_data, { cklstSn }) => {
queryClient.invalidateQueries({ queryKey: cklstMngBaseKey });
queryClient.invalidateQueries({
queryKey: [...cklstMngBaseKey, cklstSn],
});
},
});
}
export function useDeleteCklstMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (cklstSn: number) => cklstMngService.remove(cklstSn),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: cklstMngBaseKey });
},
});
}
export function useCklstArtclPage(params: CklstArtclSearchParams) {
const { cklstSn } = params;
return useQuery({
queryKey: [...cklstArtclBaseKey, "page", params],
queryFn: () => cklstArtclService.findPage(params),
enabled: cklstSn != null,
});
}
export function useCklstArtclList(cklstSn: number | null | undefined) {
return useQuery({
queryKey: [...cklstArtclBaseKey, "by-cklst", cklstSn],
queryFn: () => cklstArtclService.findByCklstSn(cklstSn!),
enabled: cklstSn != null,
});
}
export function useCklstArtcl(params: {
cklstArtclSn: number | null | undefined;
cklstSn: number | null | undefined;
}) {
const { cklstArtclSn, cklstSn } = params;
return useQuery({
queryKey: [...cklstArtclBaseKey, cklstArtclSn, cklstSn],
queryFn: () => cklstArtclService.findById(cklstArtclSn!, cklstSn!),
enabled: cklstArtclSn != null && cklstSn != null,
});
}
export function useCreateCklstArtcl() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (body: CklstArtclSaveRequest) => cklstArtclService.create(body),
onSuccess: (_data, { cklstSn }) => {
queryClient.invalidateQueries({ queryKey: cklstArtclBaseKey });
queryClient.invalidateQueries({
queryKey: [...cklstArtclBaseKey, "page", { cklstSn }],
});
queryClient.invalidateQueries({
queryKey: [...cklstArtclBaseKey, "by-cklst", cklstSn],
});
},
});
}
export function useUpdateCklstArtcl() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
cklstArtclSn,
cklstSn,
body,
}: {
cklstArtclSn: number;
cklstSn: number;
body: CklstArtclUpdateRequest;
}) => cklstArtclService.update(cklstArtclSn, cklstSn, body),
onSuccess: (_data, { cklstArtclSn, cklstSn }) => {
queryClient.invalidateQueries({ queryKey: cklstArtclBaseKey });
queryClient.invalidateQueries({
queryKey: [...cklstArtclBaseKey, cklstArtclSn, cklstSn],
});
queryClient.invalidateQueries({
queryKey: [...cklstArtclBaseKey, "page", { cklstSn }],
});
queryClient.invalidateQueries({
queryKey: [...cklstArtclBaseKey, "by-cklst", cklstSn],
});
},
});
}
export function useDeleteCklstArtcl() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
cklstArtclSn,
cklstSn,
}: {
cklstArtclSn: number;
cklstSn: number;
}) => cklstArtclService.remove(cklstArtclSn, cklstSn),
onSuccess: (_data, { cklstSn }) => {
queryClient.invalidateQueries({ queryKey: cklstArtclBaseKey });
queryClient.invalidateQueries({
queryKey: [...cklstArtclBaseKey, "page", { cklstSn }],
});
queryClient.invalidateQueries({
queryKey: [...cklstArtclBaseKey, "by-cklst", cklstSn],
});
},
});
}
+29
View File
@@ -0,0 +1,29 @@
export { cklstArtclService } from "./api/cklst-artcl-service";
export { cklstMngService } from "./api/cklst-mng-service";
export {
useCklstArtcl,
useCklstArtclList,
useCklstArtclPage,
useCklstMng,
useCklstMngPage,
useCreateCklstArtcl,
useCreateCklstMng,
useDeleteCklstArtcl,
useDeleteCklstMng,
useUpdateCklstArtcl,
useUpdateCklstMng,
} from "./api/queries";
export type {
CklstArtcl,
CklstArtclId,
CklstArtclSaveRequest,
CklstArtclSearchParams,
CklstArtclService,
CklstArtclUpdateRequest,
} from "./model/cklst-artcl-interface";
export type {
CklstMng,
CklstMngSaveRequest,
CklstMngSearchParams,
CklstMngService,
} from "./model/cklst-mng-interface";
@@ -0,0 +1,48 @@
import type { PageableParams, PageResponse } from "@/shared/model";
export interface CklstArtclId {
cklstArtclSn: number;
cklstSn: number;
}
export interface CklstArtcl {
cklstArtclSn: number;
cklstSn: number;
cklstArtclNm?: string;
useYn?: "Y" | "N";
rgtr?: string;
rgtrNm?: string;
mdfr?: string;
regDt?: string;
mdfcnDt?: string;
}
export interface CklstArtclSearchParams extends PageableParams {
cklstSn?: number;
cklstArtclNm?: string;
useYn?: "Y" | "N";
}
export interface CklstArtclSaveRequest {
cklstSn: number;
cklstArtclNm?: string;
useYn?: "Y" | "N";
}
export interface CklstArtclUpdateRequest {
cklstArtclNm?: string;
useYn?: "Y" | "N";
}
export interface CklstArtclService {
findPage(params: CklstArtclSearchParams): Promise<PageResponse<CklstArtcl>>;
findByCklstSn(cklstSn: number): Promise<CklstArtcl[]>;
findById(cklstArtclSn: number, cklstSn: number): Promise<CklstArtcl>;
create(body: CklstArtclSaveRequest): Promise<CklstArtclId>;
update(
cklstArtclSn: number,
cklstSn: number,
body: CklstArtclUpdateRequest,
): Promise<CklstArtclId>;
remove(cklstArtclSn: number, cklstSn: number): Promise<CklstArtclId>;
}
@@ -0,0 +1,36 @@
import type { PageableParams, PageResponse } from "@/shared/model";
export interface CklstMng {
cklstSn: number;
cklstNm: string;
taskSeComCd: string;
useYn?: "Y" | "N";
docSeComCd?: string;
rgtr?: string;
rgtrNm?: string;
mdfr?: string;
regDt?: string;
mdfcnDt?: string;
}
export interface CklstMngSearchParams extends PageableParams {
cklstNm?: string;
taskSeComCd?: string;
useYn?: "Y" | "N";
docSeComCd?: string;
}
export interface CklstMngSaveRequest {
cklstNm: string;
taskSeComCd: string;
useYn?: "Y" | "N";
docSeComCd?: string;
}
export interface CklstMngService {
findPage(params: CklstMngSearchParams): Promise<PageResponse<CklstMng>>;
findById(cklstSn: number): Promise<CklstMng>;
create(body: CklstMngSaveRequest): Promise<number>;
update(cklstSn: number, body: CklstMngSaveRequest): Promise<number>;
remove(cklstSn: number): Promise<number>;
}
@@ -1,4 +1,4 @@
import { client } from "@/shared/api/client";
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { Code, CodeService } from "../model/code-interface";
@@ -9,9 +9,7 @@ async function getList(upCd: string): Promise<Code[]> {
}
async function getListByUseYn(upCd: string, useYn: string): Promise<Code[]> {
const response = await client.get<Code[]>(
`${ENDPOINT.CMS_SERVICE.CODE}/${upCd}/${useYn}`,
);
const response = await client.get<Code[]>(`${ENDPOINT.CMS_SERVICE.CODE}/${upCd}/${useYn}`);
return response.data;
}
@@ -1,4 +1,4 @@
import { client } from "@/shared/api/client";
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
@@ -11,17 +11,14 @@ import type {
} from "../model/elapr-apln-interface";
async function findPage(params: ElaprAplnListParams): Promise<PageResponse<ElaprApln>> {
const response = await client.get<PageResponse<ElaprApln>>(
ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN,
{ params },
);
const response = await client.get<PageResponse<ElaprApln>>(ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN, {
params,
});
return response.data;
}
async function findById(aplnId: string): Promise<ElaprApln> {
const response = await client.get<ElaprApln>(
`${ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN}/${aplnId}`,
);
const response = await client.get<ElaprApln>(`${ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN}/${aplnId}`);
return response.data;
}
@@ -1,4 +1,4 @@
import { client } from "@/shared/api/client";
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
@@ -26,17 +26,11 @@ async function findById(elaprTmpltId: string): Promise<ElaprTmplt> {
}
async function create(body: ElaprTmpltSaveRequest): Promise<ElaprTmplt> {
const response = await client.post<ElaprTmplt>(
ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT,
body,
);
const response = await client.post<ElaprTmplt>(ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT, body);
return response.data;
}
async function update(
elaprTmpltId: string,
body: ElaprTmpltUpdateRequest,
): Promise<ElaprTmplt> {
async function update(elaprTmpltId: string, body: ElaprTmpltUpdateRequest): Promise<ElaprTmplt> {
const response = await client.put<ElaprTmplt>(
`${ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT}/${elaprTmpltId}`,
body,
@@ -1,2 +0,0 @@
export { AprvrPickerDialog } from "./ui/aprvr-picker-dialog";
export type { ReviewerOption } from "./model/mock-reviewers";
@@ -1,2 +0,0 @@
export { ElaprTmpltFormDialog } from "./ui/elapr-tmplt-form-dialog";
export type { ElaprTmpltFormValues } from "./ui/elapr-tmplt-form-dialog";
@@ -0,0 +1,6 @@
import type { ElaprAplnRow } from "../model/apln-row-types";
/** 그리드 행 순서(1-based)로 결재순서번호 일괄 채번 */
export function assignAtrzSeqNumbers(rows: ElaprAplnRow[]): ElaprAplnRow[] {
return rows.map((row, index) => ({ ...row, atrzSeqNo: index + 1 }));
}
@@ -6,7 +6,10 @@ import { arrayMove } from "@dnd-kit/sortable";
import type { ElaprApln } from "@/entities/elapr";
import type { ElaprAplnRow } from "../model/types";
import type { ElaprAplnRow } from "../model/apln-row-types";
import { assignAtrzSeqNumbers } from "./assign-atrz-seq";
import { validateAtrzOrder } from "./validators";
function toRow(item: ElaprApln): ElaprAplnRow {
return { ...item, rowKey: item.aplnId, isNew: false, isDirty: false };
@@ -41,14 +44,12 @@ export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) {
if (!elaprTmpltId || isEditingLocked) return;
const rowKey = createDraftRowKey();
const nextSeq = rows.length + 1;
setRows((prev) => [
...prev,
{
rowKey,
elaprTmpltId,
atrzSeqNo: nextSeq,
atrzSeComCd: "",
aprvrUuid: "",
aprvrNm: "",
@@ -57,7 +58,7 @@ export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) {
},
]);
setEditingRowKey(rowKey);
}, [elaprTmpltId, isEditingLocked, rows.length]);
}, [elaprTmpltId, isEditingLocked]);
const startEdit = useCallback(
(rowKey: string) => {
@@ -110,26 +111,36 @@ export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) {
}, []);
const reorderRows = useCallback(
(activeKey: string, overKey: string): ElaprAplnRow[] => {
if (isEditingLocked) return [];
(
activeKey: string,
overKey: string,
): { payloadRows?: ElaprAplnRow[]; orderError?: string | null } => {
if (isEditingLocked) return {};
let newRows: ElaprAplnRow[] = [];
let payloadRows: ElaprAplnRow[] = [];
let orderError: string | null = null;
setRows((prev) => {
const oldIndex = prev.findIndex((r) => r.rowKey === activeKey);
const newIndex = prev.findIndex((r) => r.rowKey === overKey);
if (oldIndex < 0 || newIndex < 0) return prev;
const moved = arrayMove(prev, oldIndex, newIndex).map((row, idx) => ({
const moved = assignAtrzSeqNumbers(arrayMove(prev, oldIndex, newIndex)).map((row) => ({
...row,
atrzSeqNo: idx + 1,
isDirty: true,
}));
newRows = moved;
const error = validateAtrzOrder(moved);
if (error) {
orderError = error;
return prev;
}
payloadRows = moved;
return moved;
});
return newRows;
return { payloadRows, orderError };
},
[isEditingLocked],
);
@@ -1,9 +1,4 @@
import {
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from "@dnd-kit/core";
import { KeyboardSensor, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
export function useDndSensors() {
@@ -0,0 +1,61 @@
import type { ElaprAplnRow } from "../model/apln-row-types";
export const ATRZ_REVIEWER_CD = "ATRZ01";
export const ATRZ_FINAL_CD = "ATRZ03";
/** 검토자 최상단 · 최종승인자 최하단 순서 검증 */
export function validateAtrzOrder(rows: ElaprAplnRow[]): string | null {
const reviewerIndices = rows
.map((row, index) => (row.atrzSeComCd === ATRZ_REVIEWER_CD ? index : -1))
.filter((index) => index >= 0);
if (reviewerIndices.length > 0) {
const lastReviewerIndex = Math.max(...reviewerIndices);
const hasNonReviewerAbove = rows
.slice(0, lastReviewerIndex)
.some((row) => row.atrzSeComCd && row.atrzSeComCd !== ATRZ_REVIEWER_CD);
if (hasNonReviewerAbove) {
return "검토자는 결재선 최상단에만 배치할 수 있습니다.";
}
}
const finalIndices = rows
.map((row, index) => (row.atrzSeComCd === ATRZ_FINAL_CD ? index : -1))
.filter((index) => index >= 0);
if (finalIndices.length > 0) {
const firstFinalIndex = Math.min(...finalIndices);
const hasNonFinalBelow = rows
.slice(firstFinalIndex + 1)
.some((row) => row.atrzSeComCd && row.atrzSeComCd !== ATRZ_FINAL_CD);
if (hasNonFinalBelow) {
return "최종승인자는 결재선 최하단에만 배치할 수 있습니다.";
}
}
return null;
}
export function validateAplnRow(row: ElaprAplnRow): string | null {
if (!row.atrzSeComCd?.trim()) {
return "결재구분을 선택해 주세요.";
}
if (!row.aprvrUuid?.trim()) {
return "결재자를 선택해 주세요.";
}
return null;
}
export function validateUniqueSeqNo(rows: ElaprAplnRow[]): string | null {
const seqSet = new Set<number>();
for (const row of rows) {
if (row.atrzSeqNo == null) continue;
if (seqSet.has(row.atrzSeqNo)) {
return "동일 서식 내 결재순서번호가 중복됩니다.";
}
seqSet.add(row.atrzSeqNo);
}
return null;
}
@@ -3,8 +3,9 @@
import { useState } from "react";
import type { ElaprTmplt } from "@/entities/elapr";
import { ElaprAplnGrid } from "@/widgets/elapr-apln-grid";
import { ElaprTmpltList } from "@/widgets/elapr-tmplt-list";
import { ElaprAplnGrid } from "./elapr-apln-grid";
import { ElaprTmpltList } from "./elapr-tmplt-list";
function ApprovalLineManagementPage() {
const [selectedTmplt, setSelectedTmplt] = useState<ElaprTmplt | null>(null);
@@ -1,6 +1,9 @@
"use client";
import { useMemo, useState } from "react";
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import {
Button,
DataTable,
@@ -11,11 +14,6 @@ import {
DialogTitle,
Input,
} from "@common/components";
import {
createColumnHelper,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import type { ReviewerOption } from "../model/mock-reviewers";
import { createMockReviewers } from "../model/mock-reviewers";
@@ -36,8 +34,7 @@ function AprvrPickerDialog({ open, onOpenChange, onSelect }: AprvrPickerDialogPr
const q = keyword.trim().toLowerCase();
if (!q) return reviewers;
return reviewers.filter(
(r) =>
r.name.toLowerCase().includes(q) || r.deptNm.toLowerCase().includes(q),
(r) => r.name.toLowerCase().includes(q) || r.deptNm.toLowerCase().includes(q),
);
}, [keyword, reviewers]);
@@ -12,10 +12,9 @@ import {
useElaprAplnPage,
useUpdateElaprApln,
} from "@/entities/elapr";
import { AprvrPickerDialog, type ReviewerOption } from "@/features/aprvr-picker";
import {
Button,
LoadingWithAnimation,
Skeleton,
Table,
TableBody,
TableHead,
@@ -23,11 +22,14 @@ import {
TableRow,
} from "@common/components";
import { assignAtrzSeqNumbers } from "../lib/assign-atrz-seq";
import { useAplnRows } from "../lib/use-apln-rows";
import { useDndSensors } from "../lib/use-dnd-sensors";
import { validateAplnRow, validateUniqueSeqNo } from "../lib/validators";
import type { ElaprAplnRow } from "../model/types";
import { validateAplnRow, validateAtrzOrder } from "../lib/validators";
import type { ElaprAplnRow } from "../model/apln-row-types";
import type { ReviewerOption } from "../model/mock-reviewers";
import { AprvrPickerDialog } from "./aprvr-picker-dialog";
import { SortableAplnRow } from "./sortable-apln-row";
interface ElaprAplnGridProps {
@@ -73,7 +75,15 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (!over || active.id === over.id) return;
reorderRows(String(active.id), String(over.id));
const { orderError } = reorderRows(String(active.id), String(over.id));
if (orderError) {
alert(orderError);
}
};
const handleRowChange = (row: ElaprAplnRow, patch: Partial<ElaprAplnRow>) => {
updateRow(row.rowKey, patch);
};
const handleRowSave = async (row: ElaprAplnRow) => {
@@ -82,29 +92,34 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
alert(fieldError);
return;
}
const dupError = validateUniqueSeqNo(rows.map((r) => (r.rowKey === row.rowKey ? row : r)));
if (dupError) {
alert(dupError);
const rowsWithSeq = assignAtrzSeqNumbers(rows.map((r) => (r.rowKey === row.rowKey ? row : r)));
const savedRow = rowsWithSeq.find((r) => r.rowKey === row.rowKey);
if (!savedRow) return;
const orderError = validateAtrzOrder(rowsWithSeq);
if (orderError) {
alert(orderError);
return;
}
if (!elaprTmpltId) return;
if (row.isNew) {
if (savedRow.isNew) {
await createMutation.mutateAsync({
elaprTmpltId,
aprvrUuid: row.aprvrUuid,
atrzSeComCd: row.atrzSeComCd,
atrzSeqNo: row.atrzSeqNo,
aprvrUuid: savedRow.aprvrUuid,
atrzSeComCd: savedRow.atrzSeComCd,
atrzSeqNo: savedRow.atrzSeqNo,
});
} else if (row.aplnId) {
} else if (savedRow.aplnId) {
await updateMutation.mutateAsync({
aplnId: row.aplnId,
aplnId: savedRow.aplnId,
body: {
elaprTmpltId,
aprvrUuid: row.aprvrUuid,
atrzSeComCd: row.atrzSeComCd,
atrzSeqNo: row.atrzSeqNo,
aprvrUuid: savedRow.aprvrUuid,
atrzSeComCd: savedRow.atrzSeComCd,
atrzSeqNo: savedRow.atrzSeqNo,
},
});
}
@@ -130,13 +145,14 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
const handleBulkSave = async () => {
if (!elaprTmpltId || isEditingLocked) return;
const dupError = validateUniqueSeqNo(rows);
if (dupError) {
alert(dupError);
const rowsWithSeq = assignAtrzSeqNumbers(rows);
const orderError = validateAtrzOrder(rowsWithSeq);
if (orderError) {
alert(orderError);
return;
}
const dirtyRows = rows.filter((r) => r.isDirty || r.isNew);
const dirtyRows = rowsWithSeq.filter((r) => r.isDirty || r.isNew);
for (const row of dirtyRows) {
const fieldError = validateAplnRow(row);
@@ -198,10 +214,9 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
</div>
{aplnQuery.isPending && elaprTmpltId ? (
<div className="flex h-full w-full items-center justify-center py-8">
<div className="relative size-[4rem]">
<LoadingWithAnimation />
</div>
<div className="flex flex-col gap-2">
<Skeleton className="h-[4rem]" />
<Skeleton className="h-[16rem]" />
</div>
) : !elaprTmpltId ? (
<p className="py-8 text-center text-lg text-muted-foreground">
@@ -230,18 +245,19 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
items={rows.map((r) => r.rowKey)}
strategy={verticalListSortingStrategy}
>
{rows.map((row) => (
{rows.map((row, index) => (
<SortableAplnRow
key={row.rowKey}
row={row}
displaySeqNo={index + 1}
isEditing={editingRowKey === row.rowKey}
isLocked={isEditingLocked}
dragDisabled={isEditingLocked}
dragDisabled={isEditingLocked || !row.atrzSeComCd}
onStartEdit={() => startEdit(row.rowKey)}
onSave={() => handleRowSave(row)}
onCancel={cancelEdit}
onDelete={() => handleRowDelete(row)}
onChange={(patch) => updateRow(row.rowKey, patch)}
onChange={(patch) => handleRowChange(row, patch)}
onPickAprvr={() => {
setPickingRowKey(row.rowKey);
setPickerOpen(true);
@@ -41,13 +41,14 @@ function ElaprTmpltFormDialog({
isSubmitting,
onSubmit,
}: ElaprTmpltFormDialogProps) {
const { register, handleSubmit, reset, setValue, watch } = useForm<ElaprTmpltFormValues>({
defaultValues: {
elaprTmpltNm: "",
docSeComCd: "",
elaprTmpltExpln: "",
},
});
const { register, handleSubmit, reset, getValues, setValue, watch } =
useForm<ElaprTmpltFormValues>({
defaultValues: {
elaprTmpltNm: "",
docSeComCd: "",
elaprTmpltExpln: "",
},
});
const docSeComCd = watch("docSeComCd");
@@ -81,7 +82,7 @@ function ElaprTmpltFormDialog({
{mode === "edit" ? (
<div className="space-y-2">
<Label htmlFor="elaprTmpltId">전자결재서식아이디</Label>
<Input id="elaprTmpltId" disabled {...register("elaprTmpltId")} />
<p className="p-2 text-xl">{getValues("elaprTmpltId")}</p>
</div>
) : null}
<div className="space-y-2">
@@ -12,11 +12,12 @@ import {
useElaprTmpltPage,
useUpdateElaprTmplt,
} from "@/entities/elapr";
import { ElaprTmpltFormDialog, type ElaprTmpltFormValues } from "@/features/elapr-tmplt-dialog";
import { Button } from "@common/components";
import { createTmpltColumns } from "../model/columns";
import { ElaprTmpltFormDialog, type ElaprTmpltFormValues } from "./elapr-tmplt-form-dialog";
interface ElaprTmpltListProps {
selectedTmpltId: string | null;
onSelectTmplt: (tmplt: ElaprTmplt | null) => void;
@@ -5,12 +5,13 @@ import { CSS } from "@dnd-kit/utilities";
import { Equal } from "lucide-react";
import { CodeSelect, findCodeLabel, useCodeList } from "@/entities/code";
import { Button, Input, TableCell, TableRow } from "@common/components";
import { Button, TableCell, TableRow } from "@common/components";
import type { ElaprAplnRow } from "../model/types";
import type { ElaprAplnRow } from "../model/apln-row-types";
interface SortableAplnRowProps {
row: ElaprAplnRow;
displaySeqNo: number;
isEditing: boolean;
isLocked: boolean;
dragDisabled: boolean;
@@ -24,6 +25,7 @@ interface SortableAplnRowProps {
function SortableAplnRow({
row,
displaySeqNo,
isEditing,
isLocked,
dragDisabled,
@@ -66,21 +68,7 @@ function SortableAplnRow({
</Button>
</TableCell>
<TableCell align="center">{row.isNew ? "-" : (row.aplnId ?? "-")}</TableCell>
<TableCell align="center">
{isEditing ? (
<Input
type="number"
className="w-20"
min={1}
value={row.atrzSeqNo ?? ""}
onChange={(e) =>
onChange({ atrzSeqNo: Number.parseInt(e.target.value, 10) || undefined })
}
/>
) : (
(row.atrzSeqNo ?? "-")
)}
</TableCell>
<TableCell align="center">{displaySeqNo}</TableCell>
<TableCell align="center">
{isEditing ? (
<CodeSelect
+4
View File
@@ -14,6 +14,10 @@ export const ENDPOINT = {
ELAPR_TMPLT: "/as-elapr-service/api/v1/elapr-tmplt",
ELAPR_APLN: "/as-elapr-service/api/v1/elapr-apln",
},
AS_CHECKLIST_SERVICE: {
CKLST: "/as-checklist-service/api/v1/cklst",
CKLST_ARTCL: "/as-checklist-service/api/v1/cklst-artcl",
},
CMS_SERVICE: {
CODE: "/cms-service/api/v1/code/cache",
MENU_ROLES: "/cms-service/api/v1/menu-roles",
+10 -8
View File
@@ -1,10 +1,11 @@
import type { HTMLAttributes } from "react";
import type { UseQueryResult } from "@tanstack/react-query";
import { useReactTable, type TableOptions } from "@tanstack/react-table";
import { DataTable } from "@common/components";
import { type TableOptions, useReactTable } from "@tanstack/react-table";
import type { PageResponse } from "@/shared/model";
import { NoData } from "@/shared/ui/no-data";
import { DataTable, Skeleton } from "@common/components";
import { matchQueryStatus } from "./match-query-status";
@@ -21,11 +22,7 @@ interface UseDataTableOptions<T> {
dataTableOptions?: DataTableOptions<T>;
}
export function useDataTable<T>({
query,
tableOptions,
dataTableOptions,
}: UseDataTableOptions<T>) {
export function useDataTable<T>({ query, tableOptions, dataTableOptions }: UseDataTableOptions<T>) {
const table = useReactTable({
...tableOptions,
data: query.data?.content ?? [],
@@ -34,7 +31,12 @@ export function useDataTable<T>({
return {
tableComponent: matchQueryStatus(query, {
Pending: <p className="py-8 text-center text-sm text-muted-foreground">로딩 중…</p>,
Pending: (
<div className="flex flex-col gap-2">
<Skeleton className="h-[4rem]" />
<Skeleton className="h-[16rem]" />
</div>
),
Empty: <NoData />,
Success: () => <DataTable table={table} {...dataTableOptions} />,
}),
@@ -1 +0,0 @@
export { ElaprAplnGrid } from "./ui/elapr-apln-grid";
@@ -1,26 +0,0 @@
import type { ElaprAplnRow } from "../model/types";
export function validateAplnRow(row: ElaprAplnRow): string | null {
if (row.atrzSeqNo == null || Number.isNaN(row.atrzSeqNo)) {
return "결재순서번호를 입력해 주세요.";
}
if (!row.atrzSeComCd?.trim()) {
return "결재구분을 선택해 주세요.";
}
if (!row.aprvrUuid?.trim()) {
return "결재자를 선택해 주세요.";
}
return null;
}
export function validateUniqueSeqNo(rows: ElaprAplnRow[]): string | null {
const seqSet = new Set<number>();
for (const row of rows) {
if (row.atrzSeqNo == null) continue;
if (seqSet.has(row.atrzSeqNo)) {
return "동일 서식 내 결재순서번호가 중복됩니다.";
}
seqSet.add(row.atrzSeqNo);
}
return null;
}
@@ -1 +0,0 @@
export { ElaprTmpltList } from "./ui/elapr-tmplt-list";