diff --git a/apps/as/app/api/proxy/[...url]/route.ts b/apps/as/app/api/proxy/[...url]/route.ts index 15abf4c72..b32008942 100644 --- a/apps/as/app/api/proxy/[...url]/route.ts +++ b/apps/as/app/api/proxy/[...url]/route.ts @@ -5,6 +5,7 @@ import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, + COOKIE_DOMAIN, REFRESH_TOKEN, SERVER_API_URL, SITE_ID, @@ -20,6 +21,10 @@ async function handler( "Content-Type": "application/json", "X-Site-Id": SITE_ID, }; + const deviceId = request.headers.get("X-Device-Id"); + if (deviceId) { + headers["X-Device-Id"] = deviceId; + } let body = {}; const isRefreshHandler = params.url.includes("refresh"); @@ -39,27 +44,13 @@ async function handler( } const targetUrl = `${SERVER_API_URL}/${params.url.join("/")}`; - console.log(`[Proxy API] ${method} ${params.url.join("/")} -> ${targetUrl}`); - console.log(`[Proxy API] Headers:`, headers); - if (!isRefreshHandler) { - console.log(`[Proxy API] Body:`, body); - } - const result = await fetch(targetUrl, { method, headers, body: JSON.stringify(body), }); - console.log(`[Proxy API] Response Status: ${result.status}`); - console.log(`[Proxy API] Response Headers:`, { - [ACCESS_TOKEN]: result.headers.get(ACCESS_TOKEN), - [REFRESH_TOKEN]: result.headers.get(REFRESH_TOKEN) ? '***hidden***' : undefined, - [AUTH_USER_ID]: result.headers.get(AUTH_USER_ID), - }); - if (!result.ok) { - console.error(`[Proxy API] Request failed with status ${result.status}`); return NextResponse.json({ message: "Invalid Credentials" }, { status: 401 }); } @@ -68,24 +59,16 @@ async function handler( const userId = result.headers.get(AUTH_USER_ID); if (accessToken && userId) { - console.log(`[Proxy API] Login successful for user: ${userId}`); const response = NextResponse.json({ [ACCESS_TOKEN]: accessToken, [AUTH_USER_ID]: userId, }); - const forwardedHost = request.headers.get("x-forwarded-host"); - const host = request.headers.get("host"); - const actualHost = forwardedHost || host || new URL(request.url).hostname.toString(); - - // 운영 환경에서 domain 설정 - const domain = actualHost.includes("hasuon.or.kr") ? ".hasuon.or.kr" : actualHost; - if (refreshToken) { cookieStore.set(REFRESH_TOKEN, refreshToken, { httpOnly: true, sameSite: "strict", - domain: domain, + domain: getCookieDomain(), path: "/", }); } @@ -109,3 +92,14 @@ export async function PUT(request: Request, props: { params: Promise<{ url: stri const params = await props.params; return handler(request, params, "PUT"); } + +function getCookieDomain(): string | undefined { + if (typeof window === "undefined") return undefined; + + if (COOKIE_DOMAIN) return COOKIE_DOMAIN; + + const { hostname } = window.location; + if (hostname === "localhost" || hostname === "127.0.0.1") return undefined; + + return hostname; +} diff --git a/apps/as/package.json b/apps/as/package.json index ed4016352..cebdf28ee 100644 --- a/apps/as/package.json +++ b/apps/as/package.json @@ -32,6 +32,7 @@ "next": "15.5.18", "react-hook-form": "^7.54.2", "tus-js-client": "^4.3.1", + "uuid": "^14.0.1", "zod": "^3.24.2" }, "devDependencies": { diff --git a/apps/as/src/app/providers/index.tsx b/apps/as/src/app/providers/index.tsx index 20ec0b72e..2a0ac6e69 100644 --- a/apps/as/src/app/providers/index.tsx +++ b/apps/as/src/app/providers/index.tsx @@ -27,7 +27,9 @@ export function Providers({ children }: ProvidersProps) { - +
+ +
); } diff --git a/apps/as/src/entities/auth/api/auth-service.ts b/apps/as/src/entities/auth/api/auth-service.ts index 97b664d6f..5615f55f8 100644 --- a/apps/as/src/entities/auth/api/auth-service.ts +++ b/apps/as/src/entities/auth/api/auth-service.ts @@ -1,18 +1,16 @@ import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "@/shared/config"; import { type AuthPayload, type AuthService, getRemainingMs } from "@common/auth"; -import { - clearRefreshTimer, - removeToken, - setToken, - setupRefreshTimer, - tokenManager, -} from "../lib/token-manager"; +import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id"; +import { clearRefreshTimer, setToken, setupRefreshTimer, tokenManager } from "../lib/token-manager"; async function login(payload: AuthPayload): Promise { const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + "X-Device-Id": getOrCreateDeviceId(), + }, body: JSON.stringify(payload), }); @@ -39,6 +37,9 @@ async function silentRefresh(): Promise { try { const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`, { method: "PUT", + headers: { + "X-Device-Id": getOrCreateDeviceId(), + }, }); const data = await response.json(); @@ -72,8 +73,7 @@ async function silentRefresh(): Promise { async function logout(): Promise { tokenManager.clearAuthToken(); - removeToken("site-id-key"); - removeToken("site-theme-key"); + clearDeviceId(); if (typeof window !== "undefined") { window.dispatchEvent(new CustomEvent("auth:logout")); diff --git a/apps/as/src/entities/auth/lib/device-id.ts b/apps/as/src/entities/auth/lib/device-id.ts new file mode 100644 index 000000000..ce173b141 --- /dev/null +++ b/apps/as/src/entities/auth/lib/device-id.ts @@ -0,0 +1,17 @@ +import { v4 as uuidv4 } from "uuid"; + +const DEVICE_ID_KEY = "x-device-id"; + +export function getOrCreateDeviceId(): string { + if (typeof window === "undefined") return ""; + const existing = localStorage.getItem(DEVICE_ID_KEY); + if (existing) return existing; + const id = uuidv4(); + localStorage.setItem(DEVICE_ID_KEY, id); + return id; +} + +export function clearDeviceId(): void { + if (typeof window === "undefined") return; + localStorage.removeItem(DEVICE_ID_KEY); +} diff --git a/apps/as/src/entities/elapr/index.ts b/apps/as/src/entities/elapr/index.ts index 577742c77..adec3e931 100644 --- a/apps/as/src/entities/elapr/index.ts +++ b/apps/as/src/entities/elapr/index.ts @@ -47,6 +47,12 @@ export { findCurrentUserPendingApprovalRequest, hasCurrentUserPendingApprovalRequest, } from "./lib/approval-request-utils"; +export { + APPROVER_STATUS_MAP, + type ApproverStatusConfig, + getApproverStatus, + sortByAtrzSeqNo, +} from "./lib/approval-utils"; export { buildElaprPrgrsSttsListFromApln, buildElaprPrgrsSttsListFromAplnRows, @@ -102,3 +108,15 @@ export type { ElaprTmpltService, ElaprTmpltUpdateRequest, } from "./model/elapr-tmplt-interface"; +export type { ReviewerOption } from "./model/reviewer-option"; +export { AprvrPickerDialog } from "./ui/aprvr-picker-dialog"; +export { ElaprAplnPickerGrid } from "./ui/elapr-apln-picker-grid"; +export { ElaprApprovalDialog } from "./ui/elapr-approval-dialog"; +export { ElaprDocPrgrsStatus } from "./ui/elapr-doc-prgrs-status"; +export { + type ElaprTmpltPickerConfirmPayload, + ElaprTmpltPickerDialog, +} from "./ui/elapr-tmplt-picker-dialog"; +export { ElaprTmpltSelectTable } from "./ui/elapr-tmplt-select-table"; +export { SortableAplnPickerRow } from "./ui/sortable-apln-picker-row"; +export { SortablePrgrsSttsRow } from "./ui/sortable-prgrs-stts-row"; diff --git a/apps/as/src/entities/elapr/lib/apln-validators.ts b/apps/as/src/entities/elapr/lib/apln-validators.ts index 3ab1bcd30..91f1a95cf 100644 --- a/apps/as/src/entities/elapr/lib/apln-validators.ts +++ b/apps/as/src/entities/elapr/lib/apln-validators.ts @@ -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) { diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/approval-utils.ts b/apps/as/src/entities/elapr/lib/approval-utils.ts similarity index 78% rename from apps/as/src/pages/fnrs-cnsltn/lib/approval-utils.ts rename to apps/as/src/entities/elapr/lib/approval-utils.ts index d60d20b1c..00d7d7b2a 100644 --- a/apps/as/src/pages/fnrs-cnsltn/lib/approval-utils.ts +++ b/apps/as/src/entities/elapr/lib/approval-utils.ts @@ -1,4 +1,4 @@ -import type { ElaprDocPrgrsClientResponseDto } from "@/entities/fnrs-cnsltn"; +import type { ElaprDocPrgrsClientResponseDto } from "@/shared/model"; export interface ApproverStatusConfig { label: string; @@ -7,9 +7,10 @@ export interface ApproverStatusConfig { export const APPROVER_STATUS_MAP: Record = { ATRZ_PRGRS_001: { label: "대기", badgeClass: "bg-gray-100 text-gray-700" }, - ATRZ_PRGRS_002: { label: "요청", badgeClass: "bg-yellow-100 text-yellow-800" }, - ATRZ_PRGRS_003: { label: "승인", badgeClass: "bg-green-100 text-green-800" }, - ATRZ_PRGRS_004: { label: "반려", badgeClass: "bg-red-100 text-red-800" }, + ATRZ_PRGRS_002: { label: "요청", badgeClass: "bg-yellow-100 text-yellow-700" }, + ATRZ_PRGRS_003: { label: "승인", badgeClass: "bg-green-100 text-green-700" }, + ATRZ_PRGRS_004: { label: "반려", badgeClass: "bg-red-100 text-red-700" }, + ATRZ_PRGRS_999: { label: "결재안함", badgeClass: "bg-blue-100 text-blue-700" }, }; export function sortByAtrzSeqNo( diff --git a/apps/as/src/entities/elapr/lib/prgrs-stts-save-utils.ts b/apps/as/src/entities/elapr/lib/prgrs-stts-save-utils.ts index db6ee3e5c..6ea409344 100644 --- a/apps/as/src/entities/elapr/lib/prgrs-stts-save-utils.ts +++ b/apps/as/src/entities/elapr/lib/prgrs-stts-save-utils.ts @@ -4,6 +4,7 @@ import { } from "../model/elapr-prgrs-stts-interface"; export const PRGRS_STTS_WAITING_CD = "ATRZ_PRGRS_001"; +export const PRGRS_STTS_APPROVED_CD = "ATRZ_PRGRS_003"; /** 서버 행 비교용 최소 필드 */ export interface PrgrsSttsServerRow { @@ -11,6 +12,7 @@ export interface PrgrsSttsServerRow { aprvrUuid?: string; atrzSeComCd?: string; atrzSeqNo?: number; + dcarbYn?: string; } /** 결재선 그리드 로컬 행 — 진행상태 API 응답 + UI 보조 필드 */ @@ -29,6 +31,7 @@ export interface ElaprPrgrsSttsRow { ognzNm?: string; jbpsSeNm?: string; jbgdSeNm?: string; + dcarbYn?: string; isNew?: boolean; isDirty?: boolean; /** 소프트 삭제 — 최종 저장 시 mode:D */ @@ -41,6 +44,10 @@ export function isPrgrsSttsRowEditable(row: ElaprPrgrsSttsRow): boolean { return row.isNew === true || row.atrzPrgrsSttsComCd === PRGRS_STTS_WAITING_CD; } +export function isPrgrsSttsRowDcarbable(row: ElaprPrgrsSttsRow): boolean { + return row.atrzPrgrsSttsComCd !== PRGRS_STTS_APPROVED_CD; +} + function getVisibleRows(rows: ElaprPrgrsSttsRow[]): ElaprPrgrsSttsRow[] { return rows.filter((row) => !row.isDeleted); } @@ -85,7 +92,8 @@ function rowDiffersFromServer(row: ElaprPrgrsSttsRow, serverRow: PrgrsSttsServer return ( row.aprvrUuid !== serverRow.aprvrUuid || row.atrzSeComCd !== serverRow.atrzSeComCd || - row.atrzSeqNo !== serverRow.atrzSeqNo + row.atrzSeqNo !== serverRow.atrzSeqNo || + (row.dcarbYn ?? "N") !== (serverRow.dcarbYn ?? "N") ); } @@ -149,6 +157,7 @@ export function buildPrgrsSttsSavePayload( atrzSeComCd: row.atrzSeComCd, atrzSeqNo: row.atrzSeqNo, atrzPrgrsSttsComCd: row.atrzPrgrsSttsComCd, + dcarbYn: row.dcarbYn ?? "N", }); continue; } @@ -164,6 +173,7 @@ export function buildPrgrsSttsSavePayload( atrzSeComCd: row.atrzSeComCd, atrzSeqNo: row.atrzSeqNo, atrzPrgrsSttsComCd: row.atrzPrgrsSttsComCd, + dcarbYn: row.dcarbYn ?? "N", }); } } diff --git a/apps/as/src/entities/elapr/lib/use-prgrs-stts-rows.ts b/apps/as/src/entities/elapr/lib/use-prgrs-stts-rows.ts index 297e574a4..f4a884e75 100644 --- a/apps/as/src/entities/elapr/lib/use-prgrs-stts-rows.ts +++ b/apps/as/src/entities/elapr/lib/use-prgrs-stts-rows.ts @@ -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, @@ -34,6 +34,7 @@ function toRow(item: ElaprDocPrgrsClientResponseDto): ElaprPrgrsSttsRow { ognzNm: item.ognzNm, jbpsSeNm: item.jbpsSeNm, jbgdSeNm: item.jbgdSeNm, + dcarbYn: item.dcarbYn ?? "N", isNew: false, isDirty: false, isDeleted: false, @@ -71,6 +72,7 @@ export function usePrgrsSttsRows({ serverRows, elaprDmndId }: UsePrgrsSttsRowsOp aprvrUuid: "", aprvrNm: "", atrzPrgrsSttsComCd: PRGRS_STTS_WAITING_CD, + dcarbYn: "N", isNew: true, isDirty: true, isDeleted: false, @@ -104,6 +106,33 @@ export function usePrgrsSttsRows({ serverRows, elaprDmndId }: UsePrgrsSttsRowsOp [editingRowKey], ); + const setDcarbYn = useCallback( + (rowKey: string, checked: boolean) => { + if (editingRowKey != null && editingRowKey !== rowKey) return; + + setRows((prev) => { + const target = prev.find((r) => r.rowKey === rowKey); + if (!target || target.isDeleted) return prev; + + const nextYn = checked ? "Y" : "N"; + + return prev.map((row) => { + if (row.isDeleted) return row; + + if (row.rowKey === rowKey) { + if ((row.dcarbYn ?? "N") === nextYn) return row; + return { ...row, dcarbYn: nextYn, isDirty: true }; + } + + if (!checked) return row; + if ((row.dcarbYn ?? "N") === "N") return row; + return { ...row, dcarbYn: "N", isDirty: true }; + }); + }); + }, + [editingRowKey], + ); + const cancelEdit = useCallback(() => { if (!editingRowKey) return; @@ -202,6 +231,7 @@ export function usePrgrsSttsRows({ serverRows, elaprDmndId }: UsePrgrsSttsRowsOp addRow, startEdit, updateRow, + setDcarbYn, cancelEdit, commitEdit, markDeleted, diff --git a/apps/as/src/entities/elapr/model/elapr-apln-interface.ts b/apps/as/src/entities/elapr/model/elapr-apln-interface.ts index a41add2cd..31e0d8a25 100644 --- a/apps/as/src/entities/elapr/model/elapr-apln-interface.ts +++ b/apps/as/src/entities/elapr/model/elapr-apln-interface.ts @@ -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; diff --git a/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts b/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts index 5a3fbc689..a4eb14c7c 100644 --- a/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts +++ b/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts @@ -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 { diff --git a/apps/as/src/entities/elapr/model/elapr-prgrs-stts-interface.ts b/apps/as/src/entities/elapr/model/elapr-prgrs-stts-interface.ts index 46dad56ec..7c24ff8eb 100644 --- a/apps/as/src/entities/elapr/model/elapr-prgrs-stts-interface.ts +++ b/apps/as/src/entities/elapr/model/elapr-prgrs-stts-interface.ts @@ -22,6 +22,7 @@ export interface ElaprPrgrsSttsResponseDto { atrzPrgrsSttsComNm?: string; rjctRsn?: string; atrzDt?: string; + dcarbYn?: string; } /** 백엔드 ElaprPrgrsSttsSaveRequestDto */ @@ -33,6 +34,7 @@ export interface ElaprPrgrsSttsSaveRequestDto { atrzSeComCd?: string; atrzSeqNo?: number; atrzPrgrsSttsComCd?: string; + dcarbYn?: string; } /** 백엔드 ElaprPrgrsSttsSearchDto */ diff --git a/apps/as/src/pages/fnrs-cnsltn/model/reviewer-option.ts b/apps/as/src/entities/elapr/model/reviewer-option.ts similarity index 100% rename from apps/as/src/pages/fnrs-cnsltn/model/reviewer-option.ts rename to apps/as/src/entities/elapr/model/reviewer-option.ts diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/aprvr-picker-dialog.tsx b/apps/as/src/entities/elapr/ui/aprvr-picker-dialog.tsx similarity index 95% rename from apps/as/src/pages/fnrs-cnsltn/ui/components/aprvr-picker-dialog.tsx rename to apps/as/src/entities/elapr/ui/aprvr-picker-dialog.tsx index dd241a514..272f1f3cf 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/components/aprvr-picker-dialog.tsx +++ b/apps/as/src/entities/elapr/ui/aprvr-picker-dialog.tsx @@ -17,7 +17,7 @@ import { TableRow, } from "@common/components"; -import { type ReviewerOption } from "../../model/reviewer-option"; +import { type ReviewerOption } from "../model/reviewer-option"; const PAGE_SIZE = 500; diff --git a/apps/as/src/pages/instl-sncn/ui/components/elapr-apln-picker-grid.tsx b/apps/as/src/entities/elapr/ui/elapr-apln-picker-grid.tsx similarity index 90% rename from apps/as/src/pages/instl-sncn/ui/components/elapr-apln-picker-grid.tsx rename to apps/as/src/entities/elapr/ui/elapr-apln-picker-grid.tsx index a22a2e880..92c3d58c5 100644 --- a/apps/as/src/pages/instl-sncn/ui/components/elapr-apln-picker-grid.tsx +++ b/apps/as/src/entities/elapr/ui/elapr-apln-picker-grid.tsx @@ -6,13 +6,6 @@ import { closestCenter, DndContext, type DragEndEvent } from "@dnd-kit/core"; import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; import { useAlertStore } from "@/shared/lib"; -import { - type ElaprAplnRow, - useAplnRows, - useDndSensors, - useElaprAplnPage, - validateAplnRow, -} from "@/entities/elapr"; import { Button, Skeleton, @@ -23,7 +16,12 @@ import { TableRow, } from "@common/components"; -import type { ReviewerOption } from "../../model/reviewer-option"; +import { useElaprAplnPage } from "../api/queries"; +import { validateAplnRow } from "../lib/apln-validators"; +import { useAplnRows } from "../lib/use-apln-rows"; +import { useDndSensors } from "../lib/use-dnd-sensors"; +import type { ElaprAplnRow } from "../model/elapr-apln-row-types"; +import type { ReviewerOption } from "../model/reviewer-option"; import { AprvrPickerDialog } from "./aprvr-picker-dialog"; import { SortableAplnPickerRow } from "./sortable-apln-picker-row"; @@ -147,16 +145,14 @@ function ElaprAplnPickerGrid({ - - - + + - 결재선아이디 결재순서 결재구분 결재자 diff --git a/apps/as/src/pages/instl-sncn/ui/components/elapr-approval-dialog.tsx b/apps/as/src/entities/elapr/ui/elapr-approval-dialog.tsx similarity index 89% rename from apps/as/src/pages/instl-sncn/ui/components/elapr-approval-dialog.tsx rename to apps/as/src/entities/elapr/ui/elapr-approval-dialog.tsx index a6737ec3e..c4cb3956d 100644 --- a/apps/as/src/pages/instl-sncn/ui/components/elapr-approval-dialog.tsx +++ b/apps/as/src/entities/elapr/ui/elapr-approval-dialog.tsx @@ -9,29 +9,13 @@ import { AxiosError } from "axios"; import { ENDPOINT } from "@/shared/config"; import { useAlertStore } from "@/shared/lib"; -import { - buildPrgrsSttsSavePayload, - elaprPrgrsSttsService, - findCurrentUserPendingApprovalRequest, - hasUnsavedPrgrsSttsChanges, - isPrgrsSttsRowEditable, - useDndSensors, - useElaprDocPrgrsByDocNo, - usePrgrsSttsRows, - useSaveElaprPrgrsStts, - validateAllPrgrsSttsRows, - validateAtrzOrder, - validatePrgrsSttsRow, -} from "@/entities/elapr"; import { Button, - Checkbox, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, - Label, Skeleton, Table, TableBody, @@ -41,7 +25,20 @@ import { Textarea, } from "@common/components"; -import type { ReviewerOption } from "../../model/reviewer-option"; +import { elaprPrgrsSttsService } from "../api/elapr-prgrs-stts-service"; +import { useElaprDocPrgrsByDocNo, useSaveElaprPrgrsStts } from "../api/queries"; +import { validateAtrzOrder } from "../lib/apln-validators"; +import { findCurrentUserPendingApprovalRequest } from "../lib/approval-request-utils"; +import { + buildPrgrsSttsSavePayload, + hasUnsavedPrgrsSttsChanges, + isPrgrsSttsRowEditable, + validateAllPrgrsSttsRows, + validatePrgrsSttsRow, +} from "../lib/prgrs-stts-save-utils"; +import { useDndSensors } from "../lib/use-dnd-sensors"; +import { usePrgrsSttsRows } from "../lib/use-prgrs-stts-rows"; +import type { ReviewerOption } from "../model/reviewer-option"; import { AprvrPickerDialog } from "./aprvr-picker-dialog"; import { SortablePrgrsSttsRow } from "./sortable-prgrs-stts-row"; @@ -69,7 +66,6 @@ function ElaprApprovalDialog({ const [isProcessing, setIsProcessing] = useState(false); const [pickerOpen, setPickerOpen] = useState(false); const [pickingRowKey, setPickingRowKey] = useState(null); - const [finalApprovalYn, setFinalApprovalYn] = useState(false); const { data: elaprDocPrgrsList, isLoading } = useElaprDocPrgrsByDocNo(open ? elaprDocNo : null); @@ -83,6 +79,7 @@ function ElaprApprovalDialog({ addRow, startEdit, updateRow, + setDcarbYn, cancelEdit, commitEdit, markDeleted, @@ -200,7 +197,6 @@ function ElaprApprovalDialog({ try { await elaprPrgrsSttsService.process(currentUserPrgrs.elaprPrgrsId, { atrzPrgrsSttsComCd: "ATRZ_PRGRS_003", - finalApprovalYn, }); alert("결재가 완료되었습니다."); queryClient.invalidateQueries({ @@ -313,12 +309,13 @@ function ElaprApprovalDialog({ >
+ - - - - + + + + @@ -326,6 +323,7 @@ function ElaprApprovalDialog({ 결재순서 결재구분 결재자 + 전결자 지정 결재상태 관리 @@ -354,6 +352,7 @@ function ElaprApprovalDialog({ setPickingRowKey(row.rowKey); setPickerOpen(true); }} + onToggleDcarb={(checked) => setDcarbYn(row.rowKey, checked)} /> ))} @@ -384,16 +383,6 @@ function ElaprApprovalDialog({ {canApprove && (
-
- setFinalApprovalYn(checked === true)} - /> - -
diff --git a/apps/as/src/entities/elapr/ui/elapr-doc-prgrs-status.tsx b/apps/as/src/entities/elapr/ui/elapr-doc-prgrs-status.tsx new file mode 100644 index 000000000..731e39a07 --- /dev/null +++ b/apps/as/src/entities/elapr/ui/elapr-doc-prgrs-status.tsx @@ -0,0 +1,95 @@ +"use client"; + +import { Fragment } from "react"; + +import { ChevronRight } from "lucide-react"; + +import type { ElaprDocPrgrsClientResponseDto } from "@/shared/model"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip"; +import { cn, Skeleton } from "@common/components"; + +import { useElaprDocPrgrsByDocNo } from "../api/queries"; +import { getApproverStatus, sortByAtrzSeqNo } from "../lib/approval-utils"; + +const REJECT_STATUS_CD = "ATRZ_PRGRS_004"; + +interface ElaprDocPrgrsStatusProps { + elaprDocNo: string | null | undefined; +} + +function ElaprDocPrgrsStep({ item }: { item: ElaprDocPrgrsClientResponseDto }) { + const status = getApproverStatus(item.atrzPrgrsSttsComCd); + const isRejected = item.atrzPrgrsSttsComCd === REJECT_STATUS_CD; + const rjctRsn = item.rjctRsn?.trim(); + const metaParts = [item.ognzNm, item.jbpsSeNm].filter(Boolean); + + const statusBadge = ( + + {status.label} + + ); + + return ( +
+ {isRejected && rjctRsn ? ( + + {statusBadge} + + {rjctRsn} + + + ) : ( + statusBadge + )} +
+
+ [{item.atrzSeComNm || "-"}] + {item.aprvrNm || item.aprvrUuid || "-"} +
+ {metaParts.length > 0 && ( + {metaParts.join(" · ")} + )} +
+
+ ); +} + +function ElaprDocPrgrsStatus({ elaprDocNo }: ElaprDocPrgrsStatusProps) { + const { data, isLoading } = useElaprDocPrgrsByDocNo(elaprDocNo); + const steps = sortByAtrzSeqNo(data ?? []); + + if (isLoading) { + return ( +
+ + + +
+ ); + } + + if (steps.length === 0) { + return
결재 정보가 없습니다.
; + } + + return ( +
+ {steps.map((item, index) => ( + +
+ {index > 0 && } +
+ +
+ ))} +
+ ); +} + +export { ElaprDocPrgrsStatus }; diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/elapr-tmplt-picker-dialog.tsx b/apps/as/src/entities/elapr/ui/elapr-tmplt-picker-dialog.tsx similarity index 89% rename from apps/as/src/pages/fnrs-cnsltn/ui/components/elapr-tmplt-picker-dialog.tsx rename to apps/as/src/entities/elapr/ui/elapr-tmplt-picker-dialog.tsx index 3f59f602b..aa70932e9 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/components/elapr-tmplt-picker-dialog.tsx +++ b/apps/as/src/entities/elapr/ui/elapr-tmplt-picker-dialog.tsx @@ -1,163 +1,166 @@ -"use client"; - -import { useCallback, useState } from "react"; - -import { useAlertStore } from "@/shared/lib"; -import { - type ElaprAplnRow, - type ElaprTmplt, - getVisibleRows, - useElaprTmpltPage, - validateAllRowsForSave, - validateAtrzOrder, - validateUniqueSeqNo, -} from "@/entities/elapr"; -import { - Button, - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@common/components"; - -import { ElaprAplnPickerGrid } from "./elapr-apln-picker-grid"; -import { ElaprTmpltSelectTable } from "./elapr-tmplt-select-table"; - -const PAGE_SIZE = 100; - -interface ElaprTmpltPickerConfirmPayload { - tmplt: ElaprTmplt; - aplnRows: ElaprAplnRow[]; -} - -interface ElaprTmpltPickerDialogProps { - open: boolean; - onClose: () => void; - onSelect?: (tmplt: ElaprTmplt) => void; - onConfirm?: (payload: ElaprTmpltPickerConfirmPayload) => void; -} - -function ElaprTmpltPickerDialog({ - open, - onClose, - onSelect, - onConfirm, -}: ElaprTmpltPickerDialogProps) { - const alert = useAlertStore((state) => state.setMessage); - const [selectedTmplt, setSelectedTmplt] = useState(null); - const [aplnRows, setAplnRows] = useState([]); - const [isEditingLocked, setIsEditingLocked] = useState(false); - - const tmpltQuery = useElaprTmpltPage({ - page: 0, - size: PAGE_SIZE, - sort: "regDt,desc", - docSeComCd: "FNRS", - }); - - const handleRowsChange = useCallback((rows: ElaprAplnRow[]) => { - setAplnRows(rows); - }, []); - - const handleEditingLockedChange = useCallback((locked: boolean) => { - setIsEditingLocked(locked); - }, []); - - function handleSelectTmplt(tmplt: ElaprTmplt | null) { - setSelectedTmplt(tmplt); - setAplnRows([]); - setIsEditingLocked(false); - } - - function handleOpenChange(isOpen: boolean) { - if (!isOpen) { - setSelectedTmplt(null); - setAplnRows([]); - setIsEditingLocked(false); - onClose(); - } - } - - function handleConfirm() { - if (!selectedTmplt) { - alert("결재서식을 선택해 주세요."); - return; - } - - const rowsError = validateAllRowsForSave(aplnRows, isEditingLocked); - if (rowsError) { - alert(rowsError); - return; - } - - const orderError = validateAtrzOrder(aplnRows); - if (orderError) { - alert(orderError); - return; - } - - const seqError = validateUniqueSeqNo(aplnRows); - if (seqError) { - alert(seqError); - return; - } - - const visibleRows = getVisibleRows(aplnRows); - - const payload: ElaprTmpltPickerConfirmPayload = { - tmplt: selectedTmplt, - aplnRows: visibleRows, - }; - - if (onConfirm) { - onConfirm(payload); - } else { - onSelect?.(selectedTmplt); - } - - setSelectedTmplt(null); - setAplnRows([]); - setIsEditingLocked(false); - onClose(); - } - - return ( - - - - 결재선 선택 - - -
- - -
- - - - - -
-
- ); -} - -export { ElaprTmpltPickerDialog }; -export type { ElaprTmpltPickerConfirmPayload }; +"use client"; + +import { useCallback, useState } from "react"; + +import { useAlertStore } from "@/shared/lib"; +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@common/components"; + +import { useElaprTmpltPage } from "../api/queries"; +import { + getVisibleRows, + validateAllRowsForSave, + validateAtrzOrder, + validateUniqueSeqNo, +} from "../lib/apln-validators"; +import type { ElaprAplnRow } from "../model/elapr-apln-row-types"; +import type { ElaprTmplt } from "../model/elapr-tmplt-interface"; + +import { ElaprAplnPickerGrid } from "./elapr-apln-picker-grid"; +import { ElaprTmpltSelectTable } from "./elapr-tmplt-select-table"; + +const PAGE_SIZE = 100; + +interface ElaprTmpltPickerConfirmPayload { + tmplt: ElaprTmplt; + aplnRows: ElaprAplnRow[]; +} + +interface ElaprTmpltPickerDialogProps { + open: boolean; + onClose: () => void; + docSeComCd: string; + onSelect?: (tmplt: ElaprTmplt) => void; + onConfirm?: (payload: ElaprTmpltPickerConfirmPayload) => void; +} + +function ElaprTmpltPickerDialog({ + open, + onClose, + docSeComCd, + onSelect, + onConfirm, +}: ElaprTmpltPickerDialogProps) { + const alert = useAlertStore((state) => state.setMessage); + const [selectedTmplt, setSelectedTmplt] = useState(null); + const [aplnRows, setAplnRows] = useState([]); + const [isEditingLocked, setIsEditingLocked] = useState(false); + + const tmpltQuery = useElaprTmpltPage({ + page: 0, + size: PAGE_SIZE, + sort: "regDt,desc", + docSeComCd, + }); + + const handleRowsChange = useCallback((rows: ElaprAplnRow[]) => { + setAplnRows(rows); + }, []); + + const handleEditingLockedChange = useCallback((locked: boolean) => { + setIsEditingLocked(locked); + }, []); + + function handleSelectTmplt(tmplt: ElaprTmplt | null) { + setSelectedTmplt(tmplt); + setAplnRows([]); + setIsEditingLocked(false); + } + + function handleOpenChange(isOpen: boolean) { + if (!isOpen) { + setSelectedTmplt(null); + setAplnRows([]); + setIsEditingLocked(false); + onClose(); + } + } + + function handleConfirm() { + if (!selectedTmplt) { + alert("결재서식을 선택해 주세요."); + return; + } + + const rowsError = validateAllRowsForSave(aplnRows, isEditingLocked); + if (rowsError) { + alert(rowsError); + return; + } + + const orderError = validateAtrzOrder(aplnRows); + if (orderError) { + alert(orderError); + return; + } + + const seqError = validateUniqueSeqNo(aplnRows); + if (seqError) { + alert(seqError); + return; + } + + const visibleRows = getVisibleRows(aplnRows); + + const payload: ElaprTmpltPickerConfirmPayload = { + tmplt: selectedTmplt, + aplnRows: visibleRows, + }; + + if (onConfirm) { + onConfirm(payload); + } else { + onSelect?.(selectedTmplt); + } + + setSelectedTmplt(null); + setAplnRows([]); + setIsEditingLocked(false); + onClose(); + } + + return ( + + + + 결재선 선택 + + +
+ + +
+ + + + + +
+
+ ); +} + +export { ElaprTmpltPickerDialog }; +export type { ElaprTmpltPickerConfirmPayload }; diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/elapr-tmplt-select-table.tsx b/apps/as/src/entities/elapr/ui/elapr-tmplt-select-table.tsx similarity index 85% rename from apps/as/src/pages/fnrs-cnsltn/ui/components/elapr-tmplt-select-table.tsx rename to apps/as/src/entities/elapr/ui/elapr-tmplt-select-table.tsx index 50bdb33b9..b93ead98d 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/components/elapr-tmplt-select-table.tsx +++ b/apps/as/src/entities/elapr/ui/elapr-tmplt-select-table.tsx @@ -7,9 +7,10 @@ import { createColumnHelper, getCoreRowModel, type RowSelectionState } from "@ta import { useDataTable } from "@/shared/lib"; import type { PageResponse } from "@/shared/model"; -import type { ElaprTmplt } from "@/entities/elapr"; import { Skeleton } from "@common/components"; +import type { ElaprTmplt } from "../model/elapr-tmplt-interface"; + const columnHelper = createColumnHelper(); interface ElaprTmpltSelectTableProps { @@ -35,18 +36,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 ?? "—", - }), ], [], ); diff --git a/apps/as/src/pages/instl-sncn/ui/components/sortable-apln-picker-row.tsx b/apps/as/src/entities/elapr/ui/sortable-apln-picker-row.tsx similarity index 72% rename from apps/as/src/pages/instl-sncn/ui/components/sortable-apln-picker-row.tsx rename to apps/as/src/entities/elapr/ui/sortable-apln-picker-row.tsx index 46af147ca..90d8380b7 100644 --- a/apps/as/src/pages/instl-sncn/ui/components/sortable-apln-picker-row.tsx +++ b/apps/as/src/entities/elapr/ui/sortable-apln-picker-row.tsx @@ -4,9 +4,19 @@ 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 type { ElaprAplnRow } from "@/entities/elapr"; -import { Button, TableCell, TableRow } from "@common/components"; +import { findCodeLabel, useCodeList } from "@/entities/code"; +import { + Button, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + TableCell, + TableRow, +} from "@common/components"; + +import type { ElaprAplnRow } from "../model/elapr-apln-row-types"; interface SortableAplnPickerRowProps { row: ElaprAplnRow; @@ -36,6 +46,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 +77,35 @@ function SortableAplnPickerRow({ - {row.isNew ? "-" : (row.aplnId ?? "-")} {displaySeqNo} {isEditing ? ( - - onChange({ - atrzSeComCd: v, - atrzSeComNm: findCodeLabel(atrzSeCodes, v), - }) + onChange({ atrzSeComCd: v, atrzSeComNm: findCodeLabel(selectableAtrzSeCodes, v) }) } - placeholder="결재구분" - /> + > + + + + + {selectableAtrzSeCodes?.map((code) => ( + + {code.lwpoCdNm} + + ))} + + ) : ( atrzSeLabel || "-" )} -
- {row.aprvrNm || row.aprvrUuid || "-"} +
+ {row.aprvrNm || row.aprvrUuid || ""} {isEditing && ( - )} diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/sortable-prgrs-stts-row.tsx b/apps/as/src/entities/elapr/ui/sortable-prgrs-stts-row.tsx similarity index 74% rename from apps/as/src/pages/fnrs-cnsltn/ui/components/sortable-prgrs-stts-row.tsx rename to apps/as/src/entities/elapr/ui/sortable-prgrs-stts-row.tsx index 795c9d08d..601d272f2 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/components/sortable-prgrs-stts-row.tsx +++ b/apps/as/src/entities/elapr/ui/sortable-prgrs-stts-row.tsx @@ -5,11 +5,26 @@ 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 { type ElaprPrgrsSttsRow, isPrgrsSttsRowEditable } from "@/entities/elapr"; -import { Button, cn, TableCell, TableRow } from "@common/components"; +import { findCodeLabel, useCodeList } from "@/entities/code"; +import { + Button, + Checkbox, + cn, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + TableCell, + TableRow, +} from "@common/components"; -import { getApproverStatus } from "../../lib/approval-utils"; +import { getApproverStatus } from "../lib/approval-utils"; +import { + type ElaprPrgrsSttsRow, + isPrgrsSttsRowDcarbable, + isPrgrsSttsRowEditable, +} from "../lib/prgrs-stts-save-utils"; interface SortablePrgrsSttsRowProps { row: ElaprPrgrsSttsRow; @@ -23,6 +38,7 @@ interface SortablePrgrsSttsRowProps { onDelete: () => void; onChange: (patch: Partial) => void; onPickAprvr: () => void; + onToggleDcarb: (checked: boolean) => void; } function SortablePrgrsSttsRow({ @@ -37,8 +53,10 @@ function SortablePrgrsSttsRow({ onDelete, onChange, onPickAprvr, + onToggleDcarb, }: 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, @@ -52,6 +70,7 @@ function SortablePrgrsSttsRow({ }; const isEditable = isPrgrsSttsRowEditable(row); + const isDcarbable = isPrgrsSttsRowDcarbable(row); const rowDisabled = isLocked && !isEditing; const atrzSeLabel = row.atrzSeComNm || findCodeLabel(atrzSeCodes, row.atrzSeComCd); const status = getApproverStatus(row.atrzPrgrsSttsComCd); @@ -89,18 +108,23 @@ function SortablePrgrsSttsRow({ {displaySeqNo} {isEditing ? ( - - onChange({ - atrzSeComCd: v, - atrzSeComNm: findCodeLabel(atrzSeCodes, v), - }) + onChange({ atrzSeComCd: v, atrzSeComNm: findCodeLabel(selectableAtrzSeCodes, v) }) } - placeholder="결재구분" - /> + > + + + + + {selectableAtrzSeCodes?.map((code) => ( + + {code.lwpoCdNm} + + ))} + + ) : ( atrzSeLabel || "-" )} @@ -123,6 +147,15 @@ function SortablePrgrsSttsRow({ )}
+ + {isDcarbable && !rowDisabled && ( + onToggleDcarb(checked === true)} + aria-label="전결자 지정" + /> + )} + {isRejectedRow && rjctRsn ? ( diff --git a/apps/as/src/entities/instl-sncn/api/instl-sncn-service.ts b/apps/as/src/entities/instl-sncn/api/instl-sncn-service.ts index 38ce8e69a..2bef72628 100644 --- a/apps/as/src/entities/instl-sncn/api/instl-sncn-service.ts +++ b/apps/as/src/entities/instl-sncn/api/instl-sncn-service.ts @@ -65,6 +65,22 @@ async function techReview(id: string, payload: InstlSncnApplySaveRequestDto): Pr await client.post(`${BASE}/${id}/tech-review`, payload); } +async function submitSupplement( + id: string, + payload: InstlSncnApplySaveRequestDto, +): Promise { + const response = await client.post(`${BASE}/${id}/submitsupplement`, payload); + return response.data; +} + +async function submitReqSupplement( + id: string, + payload: InstlSncnApplySaveRequestDto, +): Promise { + const response = await client.post(`${BASE}/${id}/reqsupplement`, payload); + return response.data; +} + async function remove(id: string): Promise { await client.delete(`${BASE}/${id}`); } @@ -80,6 +96,8 @@ export const instlSncnService = { saveApply, update, techReview, + submitSupplement, + submitReqSupplement, remove, changePrgrsSttsComCd, } satisfies InstlSncnMngService; diff --git a/apps/as/src/entities/instl-sncn/api/queries.ts b/apps/as/src/entities/instl-sncn/api/queries.ts index ac651be47..f9d94dbff 100644 --- a/apps/as/src/entities/instl-sncn/api/queries.ts +++ b/apps/as/src/entities/instl-sncn/api/queries.ts @@ -99,6 +99,32 @@ export function useInstlSncnTechReview() { }); } +export function useInstlSncnSubmitSupplement() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, payload }: { id: string; payload: InstlSncnApplySaveRequestDto }) => + instlSncnService.submitSupplement(id, payload), + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: instlSncnKeys.all }); + queryClient.invalidateQueries({ queryKey: instlSncnKeys.detail(variables.id) }); + }, + }); +} + +export function useInstlSncnSubmitReqSupplement() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, payload }: { id: string; payload: InstlSncnApplySaveRequestDto }) => + instlSncnService.submitReqSupplement(id, payload), + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: instlSncnKeys.all }); + queryClient.invalidateQueries({ queryKey: instlSncnKeys.detail(variables.id) }); + }, + }); +} + export function useInstlSncnRemove() { const queryClient = useQueryClient(); diff --git a/apps/as/src/entities/instl-sncn/index.ts b/apps/as/src/entities/instl-sncn/index.ts index 9547dd6db..946e756f1 100644 --- a/apps/as/src/entities/instl-sncn/index.ts +++ b/apps/as/src/entities/instl-sncn/index.ts @@ -5,6 +5,8 @@ export { useInstlSncnList, useInstlSncnSidoCombo, useInstlSncnSigunguCombo, + useInstlSncnSubmitReqSupplement, + useInstlSncnSubmitSupplement, useInstlSncnTechReview, } from "./api/queries"; export { @@ -28,6 +30,7 @@ export { type InstlSncnMngSearchParams, type InstlSncnRvwOpnnResponseDto, type InstlSncnRvwOpnnSaveRequestDto, + type InstlSncnSplmntDmndResponseDto, type InstlSncnSplmntDmndSaveDto, type ReviewStepCode, } from "./model/instl-sncn-interface"; diff --git a/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts b/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts index 500cfb842..6ead54c13 100644 --- a/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts +++ b/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts @@ -15,6 +15,7 @@ import type { PbadmsMngDetResponseBase, RvwOpnnResponseExtension, RvwOpnnSaveBase, + SplmntDmndResponseExtension, SplmntDmndSaveBase, } from "@/shared/model"; @@ -185,20 +186,26 @@ export interface InstlSncnRvwOpnnSaveRequestDto extends RvwOpnnSaveBase { /** 설치인가 보완요구 저장 DTO(배열 원소) */ export type InstlSncnSplmntDmndSaveDto = SplmntDmndSaveBase; +/** 설치인가 보완요구 응답 DTO */ +export type InstlSncnSplmntDmndResponseDto = InstlSncnSplmntDmndSaveDto & + SplmntDmndResponseExtension & { + instlSncnDocNo?: string; + }; + /** 설치인가 신청 - 통합 저장 요청 DTO */ export interface InstlSncnApplySaveRequestDto extends PbadmsApplySaveHeader { /** 설치인가관리 정보 */ instlSncnMng: InstlSncnMngSaveRequestDto; /** 사업규모 목록 */ - bzScaleList: InstlSncnBzScaleSaveDto[]; + bzScaleList?: InstlSncnBzScaleSaveDto[]; /** 총사업비 목록 */ - bizCstList: InstlSncnBizCstSaveRequestDto[]; + bizCstList?: InstlSncnBizCstSaveRequestDto[]; /** 체크리스트 검토결과 목록 */ - cklstResultList: InstlSncnCklstResultSaveDto[]; + cklstResultList?: InstlSncnCklstResultSaveDto[]; /** 체크리스트 검토결과 맵 (rvwStpComCd -> 목록) */ cklstResultMap?: Record; /** 필수첨부파일 목록 */ - esntlAtchFileList: InstlSncnEsntlAtchFileSaveDto[]; + esntlAtchFileList?: InstlSncnEsntlAtchFileSaveDto[]; /** 검토의견 목록 */ rvwOpnnList?: InstlSncnRvwOpnnSaveRequestDto[]; /** 보완요구 목록 */ @@ -222,8 +229,11 @@ export type InstlSncnRvwOpnnResponseDto = InstlSncnRvwOpnnSaveRequestDto & }; /** 설치인가관리 상세 응답 DTO */ -export interface InstlSncnMngDetResponse - extends PbadmsMngDetResponseBase { +export interface InstlSncnMngDetResponse extends PbadmsMngDetResponseBase< + InstlSncnMngResponseDto, + InstlSncnRvwOpnnResponseDto, + InstlSncnSplmntDmndResponseDto +> { detDto: InstlSncnMngResponseDto; bizSclList: BizSclTreeDto[]; bizCstList: BizCstTreeDto[]; @@ -239,6 +249,8 @@ export interface InstlSncnMngService { saveApply: (payload: InstlSncnApplySaveRequestDto) => Promise; update: (id: string, payload: InstlSncnMngUpdateRequestDto) => Promise; techReview: (id: string, payload: InstlSncnApplySaveRequestDto) => Promise; + submitSupplement: (id: string, payload: InstlSncnApplySaveRequestDto) => Promise; + submitReqSupplement: (id: string, payload: InstlSncnApplySaveRequestDto) => Promise; remove: (id: string) => Promise; changePrgrsSttsComCd: (payload: DocPrgrsUpdateRequestDto) => Promise; } diff --git a/apps/as/src/pages/approval-line-management/model/columns.tsx b/apps/as/src/pages/approval-line-management/model/columns.tsx index a7beb668b..35dcc4a74 100644 --- a/apps/as/src/pages/approval-line-management/model/columns.tsx +++ b/apps/as/src/pages/approval-line-management/model/columns.tsx @@ -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" }, diff --git a/apps/as/src/pages/approval-line-management/ui/approval-line-management-page.tsx b/apps/as/src/pages/approval-line-management/ui/approval-line-management-page.tsx index 11b9373ac..38b018809 100644 --- a/apps/as/src/pages/approval-line-management/ui/approval-line-management-page.tsx +++ b/apps/as/src/pages/approval-line-management/ui/approval-line-management-page.tsx @@ -11,7 +11,7 @@ function ApprovalLineManagementPage() { const [selectedTmplt, setSelectedTmplt] = useState(null); return ( -
+
- - + - + + - 결재선아이디 결재순서 결재구분 + 결재자 ID 결재자 등록일시 관리 diff --git a/apps/as/src/pages/approval-line-management/ui/sortable-apln-row.tsx b/apps/as/src/pages/approval-line-management/ui/sortable-apln-row.tsx index 94d28be22..b0a23ee1e 100644 --- a/apps/as/src/pages/approval-line-management/ui/sortable-apln-row.tsx +++ b/apps/as/src/pages/approval-line-management/ui/sortable-apln-row.tsx @@ -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 ( @@ -66,31 +76,36 @@ function SortableAplnRow({ - {row.isNew ? "-" : (row.aplnId ?? "-")} {displaySeqNo} {isEditing ? ( - - onChange({ - atrzSeComCd: v, - atrzSeComNm: findCodeLabel(atrzSeCodes, v), - }) + onChange({ atrzSeComCd: v, atrzSeComNm: findCodeLabel(selectableAtrzSeCodes, v) }) } - placeholder="결재구분" - /> + > + + + + + {selectableAtrzSeCodes?.map((code) => ( + + {code.lwpoCdNm} + + ))} + + ) : ( atrzSeLabel || "-" )} + {row.aprvrUserId ?? "-"} -
- {row.aprvrNm || row.aprvrUuid || "-"} +
+ {row.aprvrNm || row.aprvrUuid || ""} {isEditing && ( - )} diff --git a/apps/as/src/pages/checklist-management/model/build-rvw-display-rows.ts b/apps/as/src/pages/checklist-management/model/build-rvw-display-rows.ts index f82fc9a96..55e0250bc 100644 --- a/apps/as/src/pages/checklist-management/model/build-rvw-display-rows.ts +++ b/apps/as/src/pages/checklist-management/model/build-rvw-display-rows.ts @@ -49,21 +49,15 @@ export function formatTreeBranchPrefix(depth: number): string { return ""; } -function compareSortSeq(a: CklstRvwRow, b: CklstRvwRow): number { - return (a.sortSeq ?? 0) - (b.sortSeq ?? 0); -} - export function buildRvwDisplayRows(rows: CklstRvwRow[]): CklstRvwDisplayRow[] { const result: CklstRvwDisplayRow[] = []; - const roots = rows - .filter((row) => (row.rvwCnLvNo ?? 1) === 1 && row.upCklstRvwSn == null) - .sort(compareSortSeq); + const roots = rows.filter((row) => (row.rvwCnLvNo ?? 1) === 1 && row.upCklstRvwSn == null); function appendSubtree(parent: CklstRvwRow, depth: number) { - const children = rows - .filter((row) => row.upCklstRvwSn != null && row.upCklstRvwSn === parent.cklstRvwSn) - .sort(compareSortSeq); + const children = rows.filter( + (row) => row.upCklstRvwSn != null && row.upCklstRvwSn === parent.cklstRvwSn, + ); for (const child of children) { result.push({ row: child, depth }); @@ -82,7 +76,7 @@ export function buildRvwDisplayRows(rows: CklstRvwRow[]): CklstRvwDisplayRow[] { (row) => !result.some((display) => display.row.rowKey === row.rowKey), ); - for (const orphan of orphans.sort(compareSortSeq)) { + for (const orphan of orphans) { result.push({ row: orphan, depth: Math.max(0, (orphan.rvwCnLvNo ?? 1) - 1) }); } diff --git a/apps/as/src/pages/checklist-management/ui/cklst-rvw-grid.tsx b/apps/as/src/pages/checklist-management/ui/cklst-rvw-grid.tsx index 7b44ba2b5..bf9884324 100644 --- a/apps/as/src/pages/checklist-management/ui/cklst-rvw-grid.tsx +++ b/apps/as/src/pages/checklist-management/ui/cklst-rvw-grid.tsx @@ -279,12 +279,13 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) { ) : (
- - - - + + + + + - + @@ -292,6 +293,7 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) { 주요 검토내용 사용 여부 체크 사용 + 정렬 순서 구분 관리 diff --git a/apps/as/src/pages/checklist-management/ui/cklst-rvw-row.tsx b/apps/as/src/pages/checklist-management/ui/cklst-rvw-row.tsx index c8bc7cb76..c31a6ee2a 100644 --- a/apps/as/src/pages/checklist-management/ui/cklst-rvw-row.tsx +++ b/apps/as/src/pages/checklist-management/ui/cklst-rvw-row.tsx @@ -107,6 +107,20 @@ function CklstRvwRowComponent({ + + + onChange({ + sortSeq: e.target.value ? Number(e.target.value) : undefined, + isDirty: true, + }) + } + className="w-20" + /> + {levelLabel}
diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts b/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts index cc6d8d4b9..6fdba8073 100644 --- a/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts +++ b/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts @@ -2,7 +2,6 @@ export const FNRS_CNSLTN_VIEW_BUTTON_ACTION = { strg: "strg", aply: "aply", del: "del", - splmntCmptn: "splmntCmptn", fnrsCnsltnAncmnt: "fnrsCnsltnAncmnt", envoSbmsn: "envoSbmsn", rjct: "rjct", @@ -15,7 +14,6 @@ export const FNRS_CNSLTN_VIEW_BUTTON_ACTION = { dmndSplmnt: "dmndSplmnt", atrzAprv: "atrzAprv", atrzRply: "atrzRply", - grndsChck: "grndsChck", } as const; export type FnrsCnsltnViewButtonAction = @@ -33,7 +31,6 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record ({ + splmntDmndSn: item.splmntDmndSn, + splmntArtclComCd: item.splmntArtclComCd, + splmntArtclCdNm: item.splmntArtclCdNm, + splmntDmndCn: item.splmntDmndCn, + splmntDmndDt: item.splmntDmndDt, + splmntDmndAtchFileGroupId: item.splmntDmndAtchFileGroupId ?? "", + splmntDmndAtchFileNm: item.splmntDmndAtchFileInfo?.orgnlFileNm ?? "", + splmntCn: item.splmntCn ?? "", + splmntPicNm: item.splmntPicNm ?? "", + splmntCmptnDt: item.splmntCmptnDt ?? "", + splmntAtchFileGroupId: item.splmntAtchFileGroupId ?? "", + splmntAtchFileNm: item.splmntAtchFileInfo?.orgnlFileNm ?? "", + splmntAtchFileAtflId: item.splmntAtchFileInfo?.atflId ?? "", + cmptnYn: item.cmptnYn, + })) ?? [], privacyConsent: "Y", }; } @@ -130,6 +148,7 @@ export function getCreateDefaultValues( fnrsSharList: [], cklstResultList: [], esntlAtchFileList: [], + splmntDmndList: [], privacyConsent: "N", }; } diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/save-payload-utils.ts b/apps/as/src/pages/fnrs-cnsltn/lib/save-payload-utils.ts index 3c6b5e3a6..fb34d7d0e 100644 --- a/apps/as/src/pages/fnrs-cnsltn/lib/save-payload-utils.ts +++ b/apps/as/src/pages/fnrs-cnsltn/lib/save-payload-utils.ts @@ -1,150 +1,167 @@ -import type { - BizCstItem, - BizCstTreeDto, - BizSclTreeDto, - BzScaleItem, - CklstResultItem, - EsntlAtchFileItem, - EsntlAtchFileMngTreeResponseDto, - FnrsCnsltnApplySaveRequestDto, - FnrsCnsltnMngDetResponseDto, - FnrsCnsltnMngResponseDto, - FnrsCnsltnSplmntDmndItem, -} from "@/entities/fnrs-cnsltn"; -import { mapFnrsSharDetToFormList, toFnrsSharSaveList } from "@/widgets/pbadms-sprt"; - -import type { ChecklistFormState, ReviewStepCode } from "../model/view-types"; - -import { toCklstSaveDto } from "./checklist-utils"; -import { type ReviewOpinionGetFormDataResult, toRvwOpnnSaveDto } from "./review-opinion-utils"; - -export function toBzScaleSaveDto(bizSclList: BizSclTreeDto[]): BzScaleItem[] { - return bizSclList - .filter((node) => node.useYn === "Y") - .map((node) => ({ - cnstpSeCd: node.cnstpSeCd, - bscPl: node.bscPlanQty, - budgetReflct: node.bgtRfltQty, - thtiDsgn: node.tstmDesignQty, - })); -} - -export function toBizCstSaveDto(bizCstList: BizCstTreeDto[]): BizCstItem[] { - return bizCstList - .filter((node) => node.useYn === "Y") - .map((node) => ({ - acntgSeCd: node.acntgSeCd, - bscPlanAmt: node.bscPlanAmt, - bgtRfltAmt: node.bgtRfltAmt, - tstmDesignAmt: node.tstmDesignAmt, - })); -} - -export function toEsntlAtchFileSaveDto( - esntlFileList: EsntlAtchFileMngTreeResponseDto[], -): EsntlAtchFileItem[] { - const esntlMap = new Map(); - - for (const item of esntlFileList) { - if (!item.esntlAtchFileId || !item.atflCd) continue; - esntlMap.set(item.esntlAtchFileId, item.atflCd); - } - - return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({ - esntlAtchFileId, - atchFileGroupId, - })); -} - -export function buildSavePayload( - data: FnrsCnsltnMngDetResponseDto, - checklistFormState: ChecklistFormState, - reviewStep: ReviewStepCode, - prgrsSttsComCd: string, -): FnrsCnsltnApplySaveRequestDto { - const detDto = data.detDto; - if (!detDto) { - throw new Error("상세 정보(detDto)가 없습니다."); - } - - const bizSclList = data.bizSclList ?? []; - const bizCstList = data.bizCstList ?? []; - const cklstResultList = data.cklstResultList ?? []; - const esntlFileList = data.esntlFileList ?? []; - - return { - fnrsCnsltnMng: { ...detDto, prgrsSttsComCd }, - bzScaleList: toBzScaleSaveDto(bizSclList), - bizCstList: toBizCstSaveDto(bizCstList), - cklstResultList: toCklstSaveDto(cklstResultList, checklistFormState, reviewStep), - esntlAtchFileList: toEsntlAtchFileSaveDto(esntlFileList), - fnrsSharList: toFnrsSharSaveList(mapFnrsSharDetToFormList(data.fnrsSharList ?? [])), - }; -} - -export function buildTechReviewReplyPayload( - data: FnrsCnsltnMngDetResponseDto, - checklistFormState: ChecklistFormState, - reviewStep: ReviewStepCode, - prgrsSttsComCd: string, - techReViewSaveCd: string, - reviewOpinionFormData?: ReviewOpinionGetFormDataResult | null, - opnrNm?: string, -): FnrsCnsltnApplySaveRequestDto { - return { - ...buildSavePayload(data, checklistFormState, reviewStep, prgrsSttsComCd), - techReViewSaveCd, - rvwStpCd: reviewStep, - rvwOpnnList: toRvwOpnnSaveDto(data, reviewOpinionFormData, opnrNm), - }; -} - -export function buildSupplementReqPayload( - data: FnrsCnsltnMngDetResponseDto, - splmntDmndCn: string, -): FnrsCnsltnApplySaveRequestDto { - const detDto = data.detDto; - if (!detDto) { - throw new Error("상세 정보(detDto)가 없습니다."); - } - - const nextSn = - (data.splmntDmndList?.reduce((max, item) => Math.max(max, item.splmntDmndSn ?? 0), 0) ?? 0) + 1; - - const splmntDmndList: FnrsCnsltnSplmntDmndItem[] = [ - ...(data.splmntDmndList ?? []), - { - splmntDmndSn: nextSn, - splmntDmndCn, - cmptnYn: "N", - }, - ]; - - return { - fnrsCnsltnMng: detDto, - splmntDmndList, - }; -} - -export function buildSupplementSubmitPayload( - data: FnrsCnsltnMngDetResponseDto, - checklistFormState: ChecklistFormState, -): FnrsCnsltnApplySaveRequestDto { - return { - ...buildSavePayload(data, checklistFormState, "LOCAL", "SL"), - splmntDmndList: data.splmntDmndList, - }; -} - -export function buildTechReviewPayload( - det: FnrsCnsltnMngResponseDto, - cklstResultList: CklstResultItem[], -): FnrsCnsltnApplySaveRequestDto { - return { - fnrsCnsltnMng: { ...det, prgrsSttsComCd: "ER" }, - bzScaleList: [], - bizCstList: [], - cklstResultList, - esntlAtchFileList: [], - }; -} +import type { + BizCstItem, + BizCstTreeDto, + BizSclTreeDto, + BzScaleItem, + CklstResultItem, + EsntlAtchFileItem, + EsntlAtchFileMngTreeResponseDto, + FnrsCnsltnApplySaveRequestDto, + FnrsCnsltnMngDetResponseDto, + FnrsCnsltnMngResponseDto, + FnrsCnsltnSplmntDmndItem, +} from "@/entities/fnrs-cnsltn"; +import { + mapFnrsSharDetToFormList, + type SplmntDmndDialogConfirmPayload, + toFnrsSharSaveList, +} from "@/widgets/pbadms-sprt"; + +import type { ChecklistFormState, ReviewStepCode } from "../model/view-types"; + +import { toCklstSaveDto } from "./checklist-utils"; +import { type ReviewOpinionGetFormDataResult, toRvwOpnnSaveDto } from "./review-opinion-utils"; + +export function toBzScaleSaveDto(bizSclList: BizSclTreeDto[]): BzScaleItem[] { + return bizSclList + .filter((node) => node.useYn === "Y") + .map((node) => ({ + cnstpSeCd: node.cnstpSeCd, + bscPl: node.bscPlanQty, + budgetReflct: node.bgtRfltQty, + thtiDsgn: node.tstmDesignQty, + })); +} + +export function toBizCstSaveDto(bizCstList: BizCstTreeDto[]): BizCstItem[] { + return bizCstList + .filter((node) => node.useYn === "Y") + .map((node) => ({ + acntgSeCd: node.acntgSeCd, + bscPlanAmt: node.bscPlanAmt, + bgtRfltAmt: node.bgtRfltAmt, + tstmDesignAmt: node.tstmDesignAmt, + })); +} + +export function toEsntlAtchFileSaveDto( + esntlFileList: EsntlAtchFileMngTreeResponseDto[], +): EsntlAtchFileItem[] { + const esntlMap = new Map(); + + for (const item of esntlFileList) { + if (!item.esntlAtchFileId || !item.atflCd) continue; + esntlMap.set(item.esntlAtchFileId, item.atflCd); + } + + return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({ + esntlAtchFileId, + atchFileGroupId, + })); +} + +export function buildSavePayload( + data: FnrsCnsltnMngDetResponseDto, + checklistFormState: ChecklistFormState, + reviewStep: ReviewStepCode, + prgrsSttsComCd: string, +): FnrsCnsltnApplySaveRequestDto { + const detDto = data.detDto; + if (!detDto) { + throw new Error("상세 정보(detDto)가 없습니다."); + } + + const bizSclList = data.bizSclList ?? []; + const bizCstList = data.bizCstList ?? []; + const cklstResultList = data.cklstResultList ?? []; + const esntlFileList = data.esntlFileList ?? []; + + return { + fnrsCnsltnMng: { ...detDto, prgrsSttsComCd }, + bzScaleList: toBzScaleSaveDto(bizSclList), + bizCstList: toBizCstSaveDto(bizCstList), + cklstResultList: toCklstSaveDto(cklstResultList, checklistFormState, reviewStep), + esntlAtchFileList: toEsntlAtchFileSaveDto(esntlFileList), + fnrsSharList: toFnrsSharSaveList(mapFnrsSharDetToFormList(data.fnrsSharList ?? [])), + }; +} + +export function buildTechReviewReplyPayload( + data: FnrsCnsltnMngDetResponseDto, + checklistFormState: ChecklistFormState, + reviewStep: ReviewStepCode, + prgrsSttsComCd: string, + techReViewSaveCd: string, + reviewOpinionFormData?: ReviewOpinionGetFormDataResult | null, + opnrNm?: string, +): FnrsCnsltnApplySaveRequestDto { + return { + ...buildSavePayload(data, checklistFormState, reviewStep, prgrsSttsComCd), + techReViewSaveCd, + rvwStpCd: reviewStep, + rvwOpnnList: toRvwOpnnSaveDto(data, reviewOpinionFormData, opnrNm), + }; +} + +export function buildSupplementReqPayload( + data: FnrsCnsltnMngDetResponseDto, + payload: SplmntDmndDialogConfirmPayload, + checklistFormState: ChecklistFormState, +): FnrsCnsltnApplySaveRequestDto { + const existingItems: FnrsCnsltnSplmntDmndItem[] = (data.splmntDmndList ?? []).map((item) => + toSplmntDmndSaveItem(item), + ); + + const nextSn = existingItems.reduce((max, item) => Math.max(max, item.splmntDmndSn ?? 0), 0) + 1; + + const newItems: FnrsCnsltnSplmntDmndItem[] = payload.items.map((item, index) => ({ + splmntDmndSn: nextSn + index, + splmntArtclComCd: item.splmntArtclComCd, + splmntDmndCn: item.splmntDmndCn, + splmntDmndAtchFileGroupId: item.splmntDmndAtchFileGroupId, + cmptnYn: "N", + })); + + return { + ...buildSavePayload(data, checklistFormState, "LOCAL", "SL"), + splmntDmndList: [...existingItems, ...newItems], + }; +} + +export function toSplmntDmndSaveItem(item: FnrsCnsltnSplmntDmndItem): FnrsCnsltnSplmntDmndItem { + return { + splmntDmndSn: item.splmntDmndSn, + splmntArtclComCd: item.splmntArtclComCd, + splmntDmndCn: item.splmntDmndCn, + splmntDmndDt: item.splmntDmndDt, + splmntDmndAtchFileGroupId: item.splmntDmndAtchFileGroupId, + splmntCn: item.splmntCn, + splmntPicNm: item.splmntPicNm, + splmntCmptnDt: item.splmntCmptnDt, + splmntAtchFileGroupId: item.splmntAtchFileGroupId, + cmptnYn: item.cmptnYn, + }; +} + +export function buildSupplementSubmitPayload( + data: FnrsCnsltnMngDetResponseDto, + checklistFormState: ChecklistFormState, +): FnrsCnsltnApplySaveRequestDto { + return { + ...buildSavePayload(data, checklistFormState, "LOCAL", "SL"), + splmntDmndList: data.splmntDmndList, + }; +} + +export function buildTechReviewPayload( + det: FnrsCnsltnMngResponseDto, + cklstResultList: CklstResultItem[], +): FnrsCnsltnApplySaveRequestDto { + return { + fnrsCnsltnMng: { ...det, prgrsSttsComCd: "ER" }, + bzScaleList: [], + bizCstList: [], + cklstResultList, + esntlAtchFileList: [], + }; +} diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/checklist-task-panel.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/components/checklist-task-panel.tsx index 4e0100089..1e4d186e6 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/components/checklist-task-panel.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/components/checklist-task-panel.tsx @@ -2,6 +2,7 @@ import type { Dispatch, SetStateAction } from "react"; import type { CklstRvwRsltTreeDto } from "@/entities/fnrs-cnsltn"; import { + Button, cn, Table, TableBody, @@ -36,6 +37,24 @@ function ChecklistTaskPanel({ const currentUserReviewStep = userRoleCode ? ROLE_TO_REVIEW_STEP[userRoleCode] : undefined; const isStepEditable = currentUserReviewStep === selectedReviewStep; + function handleSelectAll(value: "Y" | "N") { + if (!isStepEditable) return; + setFormState((prev) => { + const nextResults = { ...prev.results }; + selectedRows + .filter((row) => row.bchckUseYn === "Y") + .forEach((row) => { + nextResults[buildChecklistRowKey(row, selectedReviewStep)] = value; + }); + return patchChecklistFormState( + prev, + { results: nextResults }, + allRows, + selectedReviewStep, + ); + }); + } + return ( <>
@@ -54,8 +73,27 @@ function ChecklistTaskPanel({ ) : null} - 예 - 아니오 + + + + + + @@ -131,7 +169,7 @@ function ChecklistTaskPanel({