[as] feat: 결재선 수정 사항 반영

This commit is contained in:
2026-07-14 12:28:22 +09:00
parent 5491c524c5
commit 7a1949fc6f
14 changed files with 102 additions and 107 deletions
@@ -2,13 +2,7 @@ import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "@/shared/confi
import { type AuthPayload, type AuthService, getRemainingMs } from "@common/auth";
import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id";
import {
clearRefreshTimer,
removeToken,
setToken,
setupRefreshTimer,
tokenManager,
} from "../lib/token-manager";
import { clearRefreshTimer, setToken, setupRefreshTimer, tokenManager } from "../lib/token-manager";
async function login(payload: AuthPayload): Promise<void> {
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
@@ -79,8 +73,6 @@ async function silentRefresh(): Promise<void> {
async function logout(): Promise<void> {
tokenManager.clearAuthToken();
removeToken("site-id-key");
removeToken("site-theme-key");
clearDeviceId();
if (typeof window !== "undefined") {
@@ -1,6 +1,5 @@
import type { ElaprAplnRow } from "../model/elapr-apln-row-types";
const ATRZ_RECEIVER_CD = "ATRZ01";
const ATRZ_FINAL_CD = "ATRZ99";
interface AtrzOrderValidateRow {
@@ -16,14 +15,6 @@ export function getVisibleRows(rows: ElaprAplnRow[]): ElaprAplnRow[] {
export function validateAtrzOrder(rows: AtrzOrderValidateRow[]): string | null {
const visibleRows = rows.filter((row) => !row.isDeleted);
const receiverCount = visibleRows.filter((row) => row.atrzSeComCd === ATRZ_RECEIVER_CD).length;
if (receiverCount === 0) {
return "접수자는 반드시 등록해야 합니다.";
}
if (receiverCount > 1) {
return "접수자는 한 명만 등록할 수 있습니다.";
}
const finalCount = visibleRows.filter((row) => row.atrzSeComCd === ATRZ_FINAL_CD).length;
if (finalCount === 0) {
return "최종승인자는 반드시 등록해야 합니다.";
@@ -32,10 +23,6 @@ export function validateAtrzOrder(rows: AtrzOrderValidateRow[]): string | null {
return "최종승인자는 한 명만 등록할 수 있습니다.";
}
if (receiverCount === 1 && visibleRows[0]?.atrzSeComCd !== ATRZ_RECEIVER_CD) {
return "접수자는 결재선 첫 번째에 배치해야 합니다.";
}
if (finalCount === 1) {
const lastRow = visibleRows[visibleRows.length - 1];
if (lastRow?.atrzSeComCd !== ATRZ_FINAL_CD) {
@@ -24,7 +24,7 @@ function toRow(item: ElaprDocPrgrsClientResponseDto): ElaprPrgrsSttsRow {
elaprPrgrsId: item.elaprPrgrsId,
elaprDmndId: item.elaprDmndId,
aprvrUuid: item.aprvrUuid,
aprvrNm: item.aprvrUuidNm,
aprvrNm: item.aprvrNm,
atrzSeComCd: item.atrzSeComCd,
atrzSeComNm: item.atrzSeComNm,
atrzSeqNo: item.atrzSeqNo,
@@ -6,14 +6,14 @@ export const ELAPR_APLN_SAVE_MODE = {
DELETE: "D",
} as const;
export type ElaprAplnSaveMode =
(typeof ELAPR_APLN_SAVE_MODE)[keyof typeof ELAPR_APLN_SAVE_MODE];
export type ElaprAplnSaveMode = (typeof ELAPR_APLN_SAVE_MODE)[keyof typeof ELAPR_APLN_SAVE_MODE];
export interface ElaprApln {
aplnId: string;
elaprTmpltId: string;
aprvrUuid?: string;
aprvrNm?: string;
aprvrUserId?: string;
atrzSeComCd?: string;
atrzSeComNm?: string;
atrzSeqNo?: number;
@@ -1,4 +1,4 @@
import type { PageableParams, PageResponse } from "@/shared/model";
import type { ElaprDocPrgrsClientResponseDto, PageableParams, PageResponse } from "@/shared/model";
import type { ElaprPrgrsSttsSaveRequestDto } from "./elapr-prgrs-stts-interface";
@@ -33,23 +33,7 @@ export interface ElaprDmndSearchDto extends PageableParams {
}
/** 백엔드 ElaprDocPrgrsResponseDto */
export interface ElaprDocPrgrsResponseDto {
elaprDocNo: string;
elaprDmndId: string;
elaprPrgrsId: string;
atrzSeqNo?: number;
atrzSeComCd: string;
atrzSeComNm?: string;
aprvrUuid?: string;
aprvrUuidNm?: string;
atrzPrgrsSttsComCd: string;
atrzPrgrsSttsComNm?: string;
rjctRsn?: string;
atrzDt?: string;
jbpsSeNm?: string;
jbgdSeNm?: string;
ognzNm?: string;
}
export type ElaprDocPrgrsResponseDto = ElaprDocPrgrsClientResponseDto;
/** Controller public 메서드명·시그니처와 동일 */
export interface ElaprDmndService {
@@ -14,10 +14,6 @@ interface CreateTmpltColumnsOptions {
export function createTmpltColumns({ onEdit, onDelete }: CreateTmpltColumnsOptions) {
return [
columnHelper.accessor("elaprTmpltId", {
header: "전자결재서식아이디",
meta: { align: "left" },
}),
columnHelper.accessor("elaprTmpltNm", {
header: "결재서식명",
meta: { align: "left" },
@@ -11,7 +11,7 @@ function ApprovalLineManagementPage() {
const [selectedTmplt, setSelectedTmplt] = useState<ElaprTmplt | null>(null);
return (
<div className="space-y-6 px-[1.8rem] pb-6">
<div className="grid grid-cols-1 gap-6 px-[1.8rem] xl:grid-cols-2">
<ElaprTmpltList
selectedTmpltId={selectedTmplt?.elaprTmpltId ?? null}
onSelectTmplt={setSelectedTmplt}
@@ -186,20 +186,20 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<Table className="w-full">
<colgroup>
<col width="10%" />
<col width="30%" />
<col width="5%" />
<col width="10%" />
<col width="10%" />
<col width="15%" />
<col width="15%" />
<col width="25%" />
<col width="20%" />
<col width="10%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead className="w-10" />
<TableHead>결재선아이디</TableHead>
<TableHead>결재순서</TableHead>
<TableHead>결재구분</TableHead>
<TableHead>결재자 ID</TableHead>
<TableHead>결재자</TableHead>
<TableHead>등록일시</TableHead>
<TableHead>관리</TableHead>
@@ -4,9 +4,18 @@ import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Equal } from "lucide-react";
import { CodeSelect, findCodeLabel, useCodeList } from "@/entities/code";
import { findCodeLabel, useCodeList } from "@/entities/code";
import type { ElaprAplnRow } from "@/entities/elapr";
import { Button, TableCell, TableRow } from "@common/components";
import {
Button,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
TableCell,
TableRow,
} from "@common/components";
interface SortableAplnRowProps {
row: ElaprAplnRow;
@@ -36,6 +45,7 @@ function SortableAplnRow({
onPickAprvr,
}: SortableAplnRowProps) {
const { data: atrzSeCodes } = useCodeList("ATRZSE");
const selectableAtrzSeCodes = atrzSeCodes?.filter((code) => code.lwpoCd !== "ATRZ01");
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: row.rowKey,
@@ -49,7 +59,7 @@ function SortableAplnRow({
};
const rowDisabled = isLocked && !isEditing;
const atrzSeLabel = row.atrzSeComNm || findCodeLabel(atrzSeCodes, row.atrzSeComCd);
const atrzSeLabel = row.atrzSeComNm || findCodeLabel(selectableAtrzSeCodes, row.atrzSeComCd);
return (
<TableRow ref={setNodeRef} style={style} className={isEditing ? "bg-accent/30" : undefined}>
@@ -66,31 +76,36 @@ function SortableAplnRow({
<Equal className="!size-6" />
</Button>
</TableCell>
<TableCell align="center">{row.isNew ? "-" : (row.aplnId ?? "-")}</TableCell>
<TableCell align="center">{displaySeqNo}</TableCell>
<TableCell align="center">
{isEditing ? (
<CodeSelect
upCd="ATRZSE"
className="min-w-[120px]"
<Select
value={row.atrzSeComCd ?? ""}
onValueChange={(v) =>
onChange({
atrzSeComCd: v,
atrzSeComNm: findCodeLabel(atrzSeCodes, v),
})
onChange({ atrzSeComCd: v, atrzSeComNm: findCodeLabel(selectableAtrzSeCodes, v) })
}
placeholder="결재구분"
/>
>
<SelectTrigger className="min-w-[120px]">
<SelectValue placeholder="결재구분" />
</SelectTrigger>
<SelectContent>
{selectableAtrzSeCodes?.map((code) => (
<SelectItem key={code.lwpoCd} value={code.lwpoCd}>
{code.lwpoCdNm}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
atrzSeLabel || "-"
)}
</TableCell>
<TableCell align="center">{row.aprvrUserId ?? "-"}</TableCell>
<TableCell align="center">
<div className="flex items-center justify-center gap-4">
<span>{row.aprvrNm || row.aprvrUuid || "-"}</span>
<div className="flex items-center justify-center gap-1">
<span>{row.aprvrNm || row.aprvrUuid || ""}</span>
{isEditing && (
<Button type="button" size="sm" onClick={onPickAprvr}>
<Button type="button" size="sm" onClick={onPickAprvr} disabled={!row.atrzSeComCd}>
선택
</Button>
)}
@@ -147,16 +147,14 @@ function ElaprAplnPickerGrid({
<Table className="w-full">
<colgroup>
<col width="10%" />
<col width="30%" />
<col width="10%" />
<col width="10%" />
<col width="30%" />
<col width="10%" />
<col width="50%" />
<col width="20%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead className="w-10" />
<TableHead>결재선아이디</TableHead>
<TableHead>결재순서</TableHead>
<TableHead>결재구분</TableHead>
<TableHead>결재자</TableHead>
@@ -35,18 +35,10 @@ function ElaprTmpltSelectTable({
const columns = useMemo(
() => [
columnHelper.accessor("elaprTmpltId", {
header: "전자결재서식아이디",
meta: { align: "left" },
}),
columnHelper.accessor("elaprTmpltNm", {
header: "결재서식명",
meta: { align: "left" },
}),
columnHelper.accessor("docSeComNm", {
header: "문서구분",
cell: ({ getValue, row }) => getValue() ?? row.original.docSeComCd ?? "—",
}),
],
[],
);
@@ -4,9 +4,18 @@ import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { MoveVertical } from "lucide-react";
import { CodeSelect, findCodeLabel, useCodeList } from "@/entities/code";
import { findCodeLabel, useCodeList } from "@/entities/code";
import type { ElaprAplnRow } from "@/entities/elapr";
import { Button, TableCell, TableRow } from "@common/components";
import {
Button,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
TableCell,
TableRow,
} from "@common/components";
interface SortableAplnPickerRowProps {
row: ElaprAplnRow;
@@ -36,6 +45,7 @@ function SortableAplnPickerRow({
onPickAprvr,
}: SortableAplnPickerRowProps) {
const { data: atrzSeCodes } = useCodeList("ATRZSE");
const selectableAtrzSeCodes = atrzSeCodes?.filter((code) => code.lwpoCd !== "ATRZ01");
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: row.rowKey,
@@ -66,31 +76,35 @@ function SortableAplnPickerRow({
<MoveVertical className="!size-6" />
</Button>
</TableCell>
<TableCell align="center">{row.isNew ? "-" : (row.aplnId ?? "-")}</TableCell>
<TableCell align="center">{displaySeqNo}</TableCell>
<TableCell align="center">
{isEditing ? (
<CodeSelect
upCd="ATRZSE"
className="min-w-[120px]"
<Select
value={row.atrzSeComCd ?? ""}
onValueChange={(v) =>
onChange({
atrzSeComCd: v,
atrzSeComNm: findCodeLabel(atrzSeCodes, v),
})
onChange({ atrzSeComCd: v, atrzSeComNm: findCodeLabel(selectableAtrzSeCodes, v) })
}
placeholder="결재구분"
/>
>
<SelectTrigger className="min-w-[120px]">
<SelectValue placeholder="결재구분" />
</SelectTrigger>
<SelectContent>
{selectableAtrzSeCodes?.map((code) => (
<SelectItem key={code.lwpoCd} value={code.lwpoCd}>
{code.lwpoCdNm}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
atrzSeLabel || "-"
)}
</TableCell>
<TableCell align="center">
<div className="flex items-center justify-center gap-4">
<span>{row.aprvrNm || row.aprvrUuid || "-"}</span>
<div className="flex items-center justify-center gap-2">
<span>{row.aprvrNm || row.aprvrUuid || ""}</span>
{isEditing && (
<Button type="button" size="sm" onClick={onPickAprvr}>
<Button type="button" size="sm" onClick={onPickAprvr} disabled={!row.atrzSeComCd}>
선택
</Button>
)}
@@ -5,9 +5,19 @@ import { CSS } from "@dnd-kit/utilities";
import { MoveVertical } from "lucide-react";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
import { CodeSelect, findCodeLabel, useCodeList } from "@/entities/code";
import { findCodeLabel, useCodeList } from "@/entities/code";
import { type ElaprPrgrsSttsRow, isPrgrsSttsRowEditable } from "@/entities/elapr";
import { Button, cn, TableCell, TableRow } from "@common/components";
import {
Button,
cn,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
TableCell,
TableRow,
} from "@common/components";
import { getApproverStatus } from "../../lib/approval-utils";
@@ -39,6 +49,7 @@ function SortablePrgrsSttsRow({
onPickAprvr,
}: SortablePrgrsSttsRowProps) {
const { data: atrzSeCodes } = useCodeList("ATRZSE");
const selectableAtrzSeCodes = atrzSeCodes?.filter((code) => code.lwpoCd !== "ATRZ01");
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: row.rowKey,
@@ -89,18 +100,23 @@ function SortablePrgrsSttsRow({
<TableCell align="center">{displaySeqNo}</TableCell>
<TableCell align="center">
{isEditing ? (
<CodeSelect
upCd="ATRZSE"
className="min-w-[120px]"
<Select
value={row.atrzSeComCd ?? ""}
onValueChange={(v) =>
onChange({
atrzSeComCd: v,
atrzSeComNm: findCodeLabel(atrzSeCodes, v),
})
onChange({ atrzSeComCd: v, atrzSeComNm: findCodeLabel(selectableAtrzSeCodes, v) })
}
placeholder="결재구분"
/>
>
<SelectTrigger className="min-w-[120px]">
<SelectValue placeholder="결재구분" />
</SelectTrigger>
<SelectContent>
{selectableAtrzSeCodes?.map((code) => (
<SelectItem key={code.lwpoCd} value={code.lwpoCd}>
{code.lwpoCdNm}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
atrzSeLabel || "-"
)}
+2 -1
View File
@@ -143,7 +143,8 @@ export interface ElaprDocPrgrsClientResponseDto {
atrzSeComCd?: string;
atrzSeComNm?: string;
aprvrUuid?: string;
aprvrUuidNm?: string;
aprvrNm?: string;
aprvrUserId?: string;
atrzPrgrsSttsComCd?: string;
atrzPrgrsSttsComNm?: string;
rjctRsn?: string | null;