Merge branch 'refs/heads/iipbs/as/dev' into feature/0612_sentry_fr_merge
# Conflicts: # apps/cs/app/api/proxy/[...url]/route.ts # pnpm-lock.yaml
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import { DesignEditPage } from "@/pages/design-cnstn";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function Page({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
return <DesignEditPage id={id} />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { DesignViewPage } from "@/pages/design-cnstn";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function Page({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
return <DesignViewPage id={id} />;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { DesignListPage } from "@/pages/design-cnstn";
|
||||
|
||||
export default DesignListPage;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { DesignRegistPage } from "@/pages/design-cnstn";
|
||||
import { parseTaskSeComCdFromApi } from "@/pages/design-cnstn/lib/design-cnstn-regist-schema";
|
||||
|
||||
interface PageProps {
|
||||
searchParams: Promise<{ taskSeComCd?: string }>;
|
||||
}
|
||||
|
||||
export default async function Page({ searchParams }: PageProps) {
|
||||
const params = await searchParams;
|
||||
const taskSeComCd = parseTaskSeComCdFromApi(params.taskSeComCd);
|
||||
return <DesignRegistPage taskSeComCd={taskSeComCd.length ? taskSeComCd : undefined} />;
|
||||
}
|
||||
@@ -68,7 +68,7 @@ async function handler(
|
||||
cookieStore.set(REFRESH_TOKEN, refreshToken, {
|
||||
httpOnly: true,
|
||||
sameSite: "strict",
|
||||
domain: getCookieDomain(),
|
||||
domain: COOKIE_DOMAIN,
|
||||
path: "/",
|
||||
});
|
||||
}
|
||||
@@ -92,14 +92,3 @@ 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;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { withSentryConfig } from "@sentry/nextjs";
|
||||
import path from "path";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
reactStrictMode: true,
|
||||
transpilePackages: ["@common/assets"],
|
||||
outputFileTracingRoot: path.join(__dirname, "../../"),
|
||||
|
||||
@@ -27,7 +27,9 @@ export function Providers({ children }: ProvidersProps) {
|
||||
</AuthWrapper>
|
||||
</ConfirmProvider>
|
||||
</StoreProvider>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
<div style={{ fontSize: "1.4rem" }}>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "@/shared/config";
|
||||
import { type AuthPayload, type AuthService, getRemainingMs } from "@common/auth";
|
||||
import { type AuthPayload, type AuthService } from "@common/auth";
|
||||
|
||||
import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id";
|
||||
import {
|
||||
clearRefreshTimer,
|
||||
removeToken,
|
||||
setToken,
|
||||
setupRefreshTimer,
|
||||
tokenManager,
|
||||
} from "../lib/token-manager";
|
||||
import { clearRefreshTimer, setToken, tokenManager } from "../lib/token-manager";
|
||||
|
||||
async function login(payload: AuthPayload): Promise<void> {
|
||||
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
|
||||
@@ -32,10 +26,10 @@ async function login(payload: AuthPayload): Promise<void> {
|
||||
setToken(CLAIM_NAME, accessToken);
|
||||
setToken(AUTH_USER_ID, tokenId);
|
||||
|
||||
const remainingMs = getRemainingMs(accessToken);
|
||||
if (remainingMs > 0) {
|
||||
setupRefreshTimer(silentRefresh, remainingMs);
|
||||
}
|
||||
// const remainingMs = getRemainingMs(accessToken);
|
||||
// if (remainingMs > 0) {
|
||||
// setupRefreshTimer(silentRefresh, remainingMs);
|
||||
// }
|
||||
}
|
||||
|
||||
async function silentRefresh(): Promise<void> {
|
||||
@@ -64,10 +58,10 @@ async function silentRefresh(): Promise<void> {
|
||||
setToken(CLAIM_NAME, accessToken);
|
||||
setToken(AUTH_USER_ID, tokenId);
|
||||
|
||||
const remainingMs = getRemainingMs(accessToken);
|
||||
if (remainingMs > 0) {
|
||||
setupRefreshTimer(silentRefresh, remainingMs);
|
||||
}
|
||||
// const remainingMs = getRemainingMs(accessToken);
|
||||
// if (remainingMs > 0) {
|
||||
// setupRefreshTimer(silentRefresh, remainingMs);
|
||||
// }
|
||||
} catch (error) {
|
||||
console.error(`[silentRefresh] Failed at ${new Date().toISOString()}`, error);
|
||||
tokenManager.clearAuthToken();
|
||||
@@ -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") {
|
||||
|
||||
@@ -18,13 +18,7 @@ interface CodeSelectProps extends React.ComponentPropsWithoutRef<typeof Select>
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function CodeSelect({
|
||||
upCd,
|
||||
useYn,
|
||||
placeholder = "선택",
|
||||
className,
|
||||
...props
|
||||
}: CodeSelectProps) {
|
||||
function CodeSelect({ upCd, useYn, placeholder = "선택", className, ...props }: CodeSelectProps) {
|
||||
const listQuery = useCodeList(useYn ? undefined : upCd);
|
||||
const listByUseYnQuery = useCodeListByUseYn(useYn ? upCd : undefined, useYn ?? "");
|
||||
|
||||
@@ -34,7 +28,7 @@ function CodeSelect({
|
||||
return (
|
||||
<Select {...props} disabled={props.disabled || isLoading}>
|
||||
<SelectTrigger className={cn("w-[12rem]", className)}>
|
||||
<SelectValue placeholder={isLoading ? "로딩 중…" : placeholder} />
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{codes?.map((item) => (
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { PageResponse } from "@/shared/model";
|
||||
|
||||
import type {
|
||||
DesignCnstnBizCstId,
|
||||
DesignCnstnBizCstResponseDto,
|
||||
DesignCnstnBizCstSaveRequestDto,
|
||||
DesignCnstnBizCstSearchDto,
|
||||
DesignCnstnBizCstService,
|
||||
DesignCnstnBizCstUpdateRequestDto,
|
||||
} from "../model/design-cnstn-biz-cst-interface";
|
||||
|
||||
const BASE = ENDPOINT.AS_SERVICE.DESIGN_CNSTN_BIZ_CST;
|
||||
|
||||
async function findPage(
|
||||
searchDto: DesignCnstnBizCstSearchDto,
|
||||
): Promise<PageResponse<DesignCnstnBizCstResponseDto>> {
|
||||
const response = await client.get<PageResponse<DesignCnstnBizCstResponseDto>>(BASE, {
|
||||
params: searchDto,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function findById(
|
||||
designCnstnDocNo: string,
|
||||
acntgSeCd: string,
|
||||
): Promise<DesignCnstnBizCstResponseDto> {
|
||||
const response = await client.get<DesignCnstnBizCstResponseDto>(
|
||||
`${BASE}/${designCnstnDocNo}/${acntgSeCd}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function save(body: DesignCnstnBizCstSaveRequestDto): Promise<DesignCnstnBizCstId> {
|
||||
const response = await client.post<DesignCnstnBizCstId>(BASE, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function update(
|
||||
designCnstnDocNo: string,
|
||||
acntgSeCd: string,
|
||||
body: DesignCnstnBizCstUpdateRequestDto,
|
||||
): Promise<void> {
|
||||
await client.put<void>(`${BASE}/${designCnstnDocNo}/${acntgSeCd}`, body);
|
||||
}
|
||||
|
||||
async function deleteDesignCnstnBizCst(designCnstnDocNo: string, acntgSeCd: string): Promise<void> {
|
||||
await client.delete<void>(`${BASE}/${designCnstnDocNo}/${acntgSeCd}`);
|
||||
}
|
||||
|
||||
export const designCnstnBizCstService = {
|
||||
findPage,
|
||||
findById,
|
||||
save,
|
||||
update,
|
||||
delete: deleteDesignCnstnBizCst,
|
||||
} satisfies DesignCnstnBizCstService;
|
||||
@@ -0,0 +1,58 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { PageResponse } from "@/shared/model";
|
||||
|
||||
import type {
|
||||
DesignCnstnBizSclId,
|
||||
DesignCnstnBizSclResponseDto,
|
||||
DesignCnstnBizSclSaveRequestDto,
|
||||
DesignCnstnBizSclSearchDto,
|
||||
DesignCnstnBizSclService,
|
||||
DesignCnstnBizSclUpdateRequestDto,
|
||||
} from "../model/design-cnstn-biz-scl-interface";
|
||||
|
||||
const BASE = ENDPOINT.AS_SERVICE.DESIGN_CNSTN_BIZ_SCL;
|
||||
|
||||
async function findPage(
|
||||
searchDto: DesignCnstnBizSclSearchDto,
|
||||
): Promise<PageResponse<DesignCnstnBizSclResponseDto>> {
|
||||
const response = await client.get<PageResponse<DesignCnstnBizSclResponseDto>>(BASE, {
|
||||
params: searchDto,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function findById(
|
||||
designCnstnDocNo: string,
|
||||
cnstpSeCd: string,
|
||||
): Promise<DesignCnstnBizSclResponseDto> {
|
||||
const response = await client.get<DesignCnstnBizSclResponseDto>(
|
||||
`${BASE}/${designCnstnDocNo}/${cnstpSeCd}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function save(body: DesignCnstnBizSclSaveRequestDto): Promise<DesignCnstnBizSclId> {
|
||||
const response = await client.post<DesignCnstnBizSclId>(BASE, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function update(
|
||||
designCnstnDocNo: string,
|
||||
cnstpSeCd: string,
|
||||
body: DesignCnstnBizSclUpdateRequestDto,
|
||||
): Promise<void> {
|
||||
await client.put<void>(`${BASE}/${designCnstnDocNo}/${cnstpSeCd}`, body);
|
||||
}
|
||||
|
||||
async function deleteDesignCnstnBizScl(designCnstnDocNo: string, cnstpSeCd: string): Promise<void> {
|
||||
await client.delete<void>(`${BASE}/${designCnstnDocNo}/${cnstpSeCd}`);
|
||||
}
|
||||
|
||||
export const designCnstnBizSclService = {
|
||||
findPage,
|
||||
findById,
|
||||
save,
|
||||
update,
|
||||
delete: deleteDesignCnstnBizScl,
|
||||
} satisfies DesignCnstnBizSclService;
|
||||
@@ -0,0 +1,63 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { PageResponse } from "@/shared/model";
|
||||
|
||||
import type {
|
||||
DesignCnstnEsntlAtchFileId,
|
||||
DesignCnstnEsntlAtchFileResponseDto,
|
||||
DesignCnstnEsntlAtchFileSaveRequestDto,
|
||||
DesignCnstnEsntlAtchFileSearchDto,
|
||||
DesignCnstnEsntlAtchFileService,
|
||||
DesignCnstnEsntlAtchFileUpdateRequestDto,
|
||||
} from "../model/design-cnstn-esntl-atch-file-interface";
|
||||
|
||||
const BASE = ENDPOINT.AS_SERVICE.DESIGN_CNSTN_ESNTL_ATCH_FILE;
|
||||
|
||||
async function findPage(
|
||||
searchDto: DesignCnstnEsntlAtchFileSearchDto,
|
||||
): Promise<PageResponse<DesignCnstnEsntlAtchFileResponseDto>> {
|
||||
const response = await client.get<PageResponse<DesignCnstnEsntlAtchFileResponseDto>>(BASE, {
|
||||
params: searchDto,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function findById(
|
||||
designCnstnDocNo: string,
|
||||
esntlAtchFileId: string,
|
||||
): Promise<DesignCnstnEsntlAtchFileResponseDto> {
|
||||
const response = await client.get<DesignCnstnEsntlAtchFileResponseDto>(
|
||||
`${BASE}/${designCnstnDocNo}/${esntlAtchFileId}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function save(
|
||||
body: DesignCnstnEsntlAtchFileSaveRequestDto,
|
||||
): Promise<DesignCnstnEsntlAtchFileId> {
|
||||
const response = await client.post<DesignCnstnEsntlAtchFileId>(BASE, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function update(
|
||||
designCnstnDocNo: string,
|
||||
esntlAtchFileId: string,
|
||||
body: DesignCnstnEsntlAtchFileUpdateRequestDto,
|
||||
): Promise<void> {
|
||||
await client.put<void>(`${BASE}/${designCnstnDocNo}/${esntlAtchFileId}`, body);
|
||||
}
|
||||
|
||||
async function deleteDesignCnstnEsntlAtchFile(
|
||||
designCnstnDocNo: string,
|
||||
esntlAtchFileId: string,
|
||||
): Promise<void> {
|
||||
await client.delete<void>(`${BASE}/${designCnstnDocNo}/${esntlAtchFileId}`);
|
||||
}
|
||||
|
||||
export const designCnstnEsntlAtchFileService = {
|
||||
findPage,
|
||||
findById,
|
||||
save,
|
||||
update,
|
||||
delete: deleteDesignCnstnEsntlAtchFile,
|
||||
} satisfies DesignCnstnEsntlAtchFileService;
|
||||
@@ -0,0 +1,61 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { PageResponse } from "@/shared/model";
|
||||
|
||||
import type {
|
||||
DesignCnstnApplySaveRequestDto,
|
||||
DesignCnstnMngDetResponseDto,
|
||||
DesignCnstnMngResponseDto,
|
||||
DesignCnstnMngSaveRequestDto,
|
||||
DesignCnstnMngSearchDto,
|
||||
DesignCnstnMngService,
|
||||
DesignCnstnMngUpdateRequestDto,
|
||||
} from "../model/design-cnstn-mng-interface";
|
||||
|
||||
const BASE = ENDPOINT.AS_SERVICE.DESIGN_CNSTN;
|
||||
|
||||
async function findPage(
|
||||
searchDto: DesignCnstnMngSearchDto,
|
||||
): Promise<PageResponse<DesignCnstnMngResponseDto>> {
|
||||
const response = await client.get<PageResponse<DesignCnstnMngResponseDto>>(BASE, {
|
||||
params: searchDto,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function findById(id: string): Promise<DesignCnstnMngDetResponseDto> {
|
||||
const response = await client.get<DesignCnstnMngDetResponseDto>(`${BASE}/${id}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function save(body: DesignCnstnMngSaveRequestDto): Promise<string> {
|
||||
const response = await client.post<string>(BASE, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function saveApply(body: DesignCnstnApplySaveRequestDto): Promise<string> {
|
||||
const response = await client.post<string>(`${BASE}/save-apply`, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function update(id: string, body: DesignCnstnMngUpdateRequestDto): Promise<void> {
|
||||
await client.put<void>(`${BASE}/${id}`, body);
|
||||
}
|
||||
|
||||
async function techReview(id: string, body: DesignCnstnApplySaveRequestDto): Promise<void> {
|
||||
await client.post<void>(`${BASE}/${id}/tech-review`, body);
|
||||
}
|
||||
|
||||
async function deleteDesignCnstnMng(id: string): Promise<void> {
|
||||
await client.delete<void>(`${BASE}/${id}`);
|
||||
}
|
||||
|
||||
export const designCnstnMngService = {
|
||||
findPage,
|
||||
findById,
|
||||
save,
|
||||
saveApply,
|
||||
update,
|
||||
techReview,
|
||||
delete: deleteDesignCnstnMng,
|
||||
} satisfies DesignCnstnMngService;
|
||||
@@ -0,0 +1,61 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { PageResponse } from "@/shared/model";
|
||||
|
||||
import type {
|
||||
DesignCnstnRvwOpnnId,
|
||||
DesignCnstnRvwOpnnResponseDto,
|
||||
DesignCnstnRvwOpnnSaveRequestDto,
|
||||
DesignCnstnRvwOpnnSearchDto,
|
||||
DesignCnstnRvwOpnnService,
|
||||
DesignCnstnRvwOpnnUpdateRequestDto,
|
||||
} from "../model/design-cnstn-rvw-opnn-interface";
|
||||
|
||||
const BASE = ENDPOINT.AS_SERVICE.DESIGN_CNSTN_RVW_OPNN;
|
||||
|
||||
async function findPage(
|
||||
searchDto: DesignCnstnRvwOpnnSearchDto,
|
||||
): Promise<PageResponse<DesignCnstnRvwOpnnResponseDto>> {
|
||||
const response = await client.get<PageResponse<DesignCnstnRvwOpnnResponseDto>>(BASE, {
|
||||
params: searchDto,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function findById(
|
||||
designCnstnDocNo: string,
|
||||
rvwOpnnSn: number,
|
||||
): Promise<DesignCnstnRvwOpnnResponseDto> {
|
||||
const response = await client.get<DesignCnstnRvwOpnnResponseDto>(
|
||||
`${BASE}/${designCnstnDocNo}/${rvwOpnnSn}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function save(body: DesignCnstnRvwOpnnSaveRequestDto): Promise<DesignCnstnRvwOpnnId> {
|
||||
const response = await client.post<DesignCnstnRvwOpnnId>(BASE, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function update(
|
||||
designCnstnDocNo: string,
|
||||
rvwOpnnSn: number,
|
||||
body: DesignCnstnRvwOpnnUpdateRequestDto,
|
||||
): Promise<void> {
|
||||
await client.put<void>(`${BASE}/${designCnstnDocNo}/${rvwOpnnSn}`, body);
|
||||
}
|
||||
|
||||
async function deleteDesignCnstnRvwOpnn(
|
||||
designCnstnDocNo: string,
|
||||
rvwOpnnSn: number,
|
||||
): Promise<void> {
|
||||
await client.delete<void>(`${BASE}/${designCnstnDocNo}/${rvwOpnnSn}`);
|
||||
}
|
||||
|
||||
export const designCnstnRvwOpnnService = {
|
||||
findPage,
|
||||
findById,
|
||||
save,
|
||||
update,
|
||||
delete: deleteDesignCnstnRvwOpnn,
|
||||
} satisfies DesignCnstnRvwOpnnService;
|
||||
@@ -0,0 +1,411 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
DesignCnstnBizCstSaveRequestDto,
|
||||
DesignCnstnBizCstSearchDto,
|
||||
DesignCnstnBizCstUpdateRequestDto,
|
||||
} from "../model/design-cnstn-biz-cst-interface";
|
||||
import type {
|
||||
DesignCnstnBizSclSaveRequestDto,
|
||||
DesignCnstnBizSclSearchDto,
|
||||
DesignCnstnBizSclUpdateRequestDto,
|
||||
} from "../model/design-cnstn-biz-scl-interface";
|
||||
import type {
|
||||
DesignCnstnEsntlAtchFileSaveRequestDto,
|
||||
DesignCnstnEsntlAtchFileSearchDto,
|
||||
DesignCnstnEsntlAtchFileUpdateRequestDto,
|
||||
} from "../model/design-cnstn-esntl-atch-file-interface";
|
||||
import type {
|
||||
DesignCnstnApplySaveRequestDto,
|
||||
DesignCnstnMngSaveRequestDto,
|
||||
DesignCnstnMngSearchDto,
|
||||
DesignCnstnMngUpdateRequestDto,
|
||||
} from "../model/design-cnstn-mng-interface";
|
||||
import type {
|
||||
DesignCnstnRvwOpnnSaveRequestDto,
|
||||
DesignCnstnRvwOpnnSearchDto,
|
||||
DesignCnstnRvwOpnnUpdateRequestDto,
|
||||
} from "../model/design-cnstn-rvw-opnn-interface";
|
||||
|
||||
import { designCnstnBizCstService } from "./design-cnstn-biz-cst-service";
|
||||
import { designCnstnBizSclService } from "./design-cnstn-biz-scl-service";
|
||||
import { designCnstnEsntlAtchFileService } from "./design-cnstn-esntl-atch-file-service";
|
||||
import { designCnstnMngService } from "./design-cnstn-mng-service";
|
||||
import { designCnstnRvwOpnnService } from "./design-cnstn-rvw-opnn-service";
|
||||
|
||||
export const designCnstnMngBaseKey = [ENDPOINT.AS_SERVICE.DESIGN_CNSTN] as const;
|
||||
export const designCnstnBizCstBaseKey = [ENDPOINT.AS_SERVICE.DESIGN_CNSTN_BIZ_CST] as const;
|
||||
export const designCnstnBizSclBaseKey = [ENDPOINT.AS_SERVICE.DESIGN_CNSTN_BIZ_SCL] as const;
|
||||
export const designCnstnEsntlAtchFileBaseKey = [
|
||||
ENDPOINT.AS_SERVICE.DESIGN_CNSTN_ESNTL_ATCH_FILE,
|
||||
] as const;
|
||||
export const designCnstnRvwOpnnBaseKey = [ENDPOINT.AS_SERVICE.DESIGN_CNSTN_RVW_OPNN] as const;
|
||||
|
||||
/* ─── DesignCnstnMng ─── */
|
||||
|
||||
export function useDesignCnstnMngPage(params: DesignCnstnMngSearchDto) {
|
||||
return useQuery({
|
||||
queryKey: [...designCnstnMngBaseKey, "findPage", params],
|
||||
queryFn: () => designCnstnMngService.findPage(params),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDesignCnstnMng(id: string | null | undefined) {
|
||||
return useQuery({
|
||||
queryKey: [...designCnstnMngBaseKey, "findById", id],
|
||||
queryFn: () => designCnstnMngService.findById(id!),
|
||||
enabled: Boolean(id),
|
||||
});
|
||||
}
|
||||
|
||||
export function useSaveDesignCnstnMng() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (body: DesignCnstnMngSaveRequestDto) => designCnstnMngService.save(body),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useSaveApplyDesignCnstnMng() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (body: DesignCnstnApplySaveRequestDto) => designCnstnMngService.saveApply(body),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateDesignCnstnMng() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, body }: { id: string; body: DesignCnstnMngUpdateRequestDto }) =>
|
||||
designCnstnMngService.update(id, body),
|
||||
onSuccess: (_data, { id }) => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...designCnstnMngBaseKey, "findById", id],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useTechReviewDesignCnstnMng() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, body }: { id: string; body: DesignCnstnApplySaveRequestDto }) =>
|
||||
designCnstnMngService.techReview(id, body),
|
||||
onSuccess: (_data, { id }) => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...designCnstnMngBaseKey, "findById", id],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteDesignCnstnMng() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => designCnstnMngService.delete(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/* ─── DesignCnstnBizCst ─── */
|
||||
|
||||
export function useDesignCnstnBizCstPage(params: DesignCnstnBizCstSearchDto) {
|
||||
return useQuery({
|
||||
queryKey: [...designCnstnBizCstBaseKey, "findPage", params],
|
||||
queryFn: () => designCnstnBizCstService.findPage(params),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDesignCnstnBizCst(
|
||||
designCnstnDocNo: string | null | undefined,
|
||||
acntgSeCd: string | null | undefined,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: [...designCnstnBizCstBaseKey, "findById", designCnstnDocNo, acntgSeCd],
|
||||
queryFn: () => designCnstnBizCstService.findById(designCnstnDocNo!, acntgSeCd!),
|
||||
enabled: Boolean(designCnstnDocNo && acntgSeCd),
|
||||
});
|
||||
}
|
||||
|
||||
export function useSaveDesignCnstnBizCst() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (body: DesignCnstnBizCstSaveRequestDto) => designCnstnBizCstService.save(body),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnBizCstBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateDesignCnstnBizCst() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
designCnstnDocNo,
|
||||
acntgSeCd,
|
||||
body,
|
||||
}: {
|
||||
designCnstnDocNo: string;
|
||||
acntgSeCd: string;
|
||||
body: DesignCnstnBizCstUpdateRequestDto;
|
||||
}) => designCnstnBizCstService.update(designCnstnDocNo, acntgSeCd, body),
|
||||
onSuccess: (_data, { designCnstnDocNo, acntgSeCd }) => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnBizCstBaseKey });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...designCnstnBizCstBaseKey, "findById", designCnstnDocNo, acntgSeCd],
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteDesignCnstnBizCst() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
designCnstnDocNo,
|
||||
acntgSeCd,
|
||||
}: {
|
||||
designCnstnDocNo: string;
|
||||
acntgSeCd: string;
|
||||
}) => designCnstnBizCstService.delete(designCnstnDocNo, acntgSeCd),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnBizCstBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/* ─── DesignCnstnBizScl ─── */
|
||||
|
||||
export function useDesignCnstnBizSclPage(params: DesignCnstnBizSclSearchDto) {
|
||||
return useQuery({
|
||||
queryKey: [...designCnstnBizSclBaseKey, "findPage", params],
|
||||
queryFn: () => designCnstnBizSclService.findPage(params),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDesignCnstnBizScl(
|
||||
designCnstnDocNo: string | null | undefined,
|
||||
cnstpSeCd: string | null | undefined,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: [...designCnstnBizSclBaseKey, "findById", designCnstnDocNo, cnstpSeCd],
|
||||
queryFn: () => designCnstnBizSclService.findById(designCnstnDocNo!, cnstpSeCd!),
|
||||
enabled: Boolean(designCnstnDocNo && cnstpSeCd),
|
||||
});
|
||||
}
|
||||
|
||||
export function useSaveDesignCnstnBizScl() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (body: DesignCnstnBizSclSaveRequestDto) => designCnstnBizSclService.save(body),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnBizSclBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateDesignCnstnBizScl() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
designCnstnDocNo,
|
||||
cnstpSeCd,
|
||||
body,
|
||||
}: {
|
||||
designCnstnDocNo: string;
|
||||
cnstpSeCd: string;
|
||||
body: DesignCnstnBizSclUpdateRequestDto;
|
||||
}) => designCnstnBizSclService.update(designCnstnDocNo, cnstpSeCd, body),
|
||||
onSuccess: (_data, { designCnstnDocNo, cnstpSeCd }) => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnBizSclBaseKey });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...designCnstnBizSclBaseKey, "findById", designCnstnDocNo, cnstpSeCd],
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteDesignCnstnBizScl() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
designCnstnDocNo,
|
||||
cnstpSeCd,
|
||||
}: {
|
||||
designCnstnDocNo: string;
|
||||
cnstpSeCd: string;
|
||||
}) => designCnstnBizSclService.delete(designCnstnDocNo, cnstpSeCd),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnBizSclBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/* ─── DesignCnstnEsntlAtchFile ─── */
|
||||
|
||||
export function useDesignCnstnEsntlAtchFilePage(params: DesignCnstnEsntlAtchFileSearchDto) {
|
||||
return useQuery({
|
||||
queryKey: [...designCnstnEsntlAtchFileBaseKey, "findPage", params],
|
||||
queryFn: () => designCnstnEsntlAtchFileService.findPage(params),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDesignCnstnEsntlAtchFile(
|
||||
designCnstnDocNo: string | null | undefined,
|
||||
esntlAtchFileId: string | null | undefined,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: [
|
||||
...designCnstnEsntlAtchFileBaseKey,
|
||||
"findById",
|
||||
designCnstnDocNo,
|
||||
esntlAtchFileId,
|
||||
],
|
||||
queryFn: () =>
|
||||
designCnstnEsntlAtchFileService.findById(designCnstnDocNo!, esntlAtchFileId!),
|
||||
enabled: Boolean(designCnstnDocNo && esntlAtchFileId),
|
||||
});
|
||||
}
|
||||
|
||||
export function useSaveDesignCnstnEsntlAtchFile() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (body: DesignCnstnEsntlAtchFileSaveRequestDto) =>
|
||||
designCnstnEsntlAtchFileService.save(body),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnEsntlAtchFileBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateDesignCnstnEsntlAtchFile() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
designCnstnDocNo,
|
||||
esntlAtchFileId,
|
||||
body,
|
||||
}: {
|
||||
designCnstnDocNo: string;
|
||||
esntlAtchFileId: string;
|
||||
body: DesignCnstnEsntlAtchFileUpdateRequestDto;
|
||||
}) => designCnstnEsntlAtchFileService.update(designCnstnDocNo, esntlAtchFileId, body),
|
||||
onSuccess: (_data, { designCnstnDocNo, esntlAtchFileId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnEsntlAtchFileBaseKey });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [
|
||||
...designCnstnEsntlAtchFileBaseKey,
|
||||
"findById",
|
||||
designCnstnDocNo,
|
||||
esntlAtchFileId,
|
||||
],
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteDesignCnstnEsntlAtchFile() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
designCnstnDocNo,
|
||||
esntlAtchFileId,
|
||||
}: {
|
||||
designCnstnDocNo: string;
|
||||
esntlAtchFileId: string;
|
||||
}) => designCnstnEsntlAtchFileService.delete(designCnstnDocNo, esntlAtchFileId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnEsntlAtchFileBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/* ─── DesignCnstnRvwOpnn ─── */
|
||||
|
||||
export function useDesignCnstnRvwOpnnPage(params: DesignCnstnRvwOpnnSearchDto) {
|
||||
return useQuery({
|
||||
queryKey: [...designCnstnRvwOpnnBaseKey, "findPage", params],
|
||||
queryFn: () => designCnstnRvwOpnnService.findPage(params),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDesignCnstnRvwOpnn(
|
||||
designCnstnDocNo: string | null | undefined,
|
||||
rvwOpnnSn: number | null | undefined,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: [...designCnstnRvwOpnnBaseKey, "findById", designCnstnDocNo, rvwOpnnSn],
|
||||
queryFn: () => designCnstnRvwOpnnService.findById(designCnstnDocNo!, rvwOpnnSn!),
|
||||
enabled: Boolean(designCnstnDocNo && rvwOpnnSn != null),
|
||||
});
|
||||
}
|
||||
|
||||
export function useSaveDesignCnstnRvwOpnn() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (body: DesignCnstnRvwOpnnSaveRequestDto) => designCnstnRvwOpnnService.save(body),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnRvwOpnnBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateDesignCnstnRvwOpnn() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
designCnstnDocNo,
|
||||
rvwOpnnSn,
|
||||
body,
|
||||
}: {
|
||||
designCnstnDocNo: string;
|
||||
rvwOpnnSn: number;
|
||||
body: DesignCnstnRvwOpnnUpdateRequestDto;
|
||||
}) => designCnstnRvwOpnnService.update(designCnstnDocNo, rvwOpnnSn, body),
|
||||
onSuccess: (_data, { designCnstnDocNo, rvwOpnnSn }) => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnRvwOpnnBaseKey });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...designCnstnRvwOpnnBaseKey, "findById", designCnstnDocNo, rvwOpnnSn],
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteDesignCnstnRvwOpnn() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
designCnstnDocNo,
|
||||
rvwOpnnSn,
|
||||
}: {
|
||||
designCnstnDocNo: string;
|
||||
rvwOpnnSn: number;
|
||||
}) => designCnstnRvwOpnnService.delete(designCnstnDocNo, rvwOpnnSn),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnRvwOpnnBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
export { designCnstnBizCstService } from "./api/design-cnstn-biz-cst-service";
|
||||
export { designCnstnBizSclService } from "./api/design-cnstn-biz-scl-service";
|
||||
export { designCnstnEsntlAtchFileService } from "./api/design-cnstn-esntl-atch-file-service";
|
||||
export { designCnstnMngService } from "./api/design-cnstn-mng-service";
|
||||
export { designCnstnRvwOpnnService } from "./api/design-cnstn-rvw-opnn-service";
|
||||
export {
|
||||
designCnstnBizCstBaseKey,
|
||||
designCnstnBizSclBaseKey,
|
||||
designCnstnEsntlAtchFileBaseKey,
|
||||
designCnstnMngBaseKey,
|
||||
designCnstnRvwOpnnBaseKey,
|
||||
useDeleteDesignCnstnBizCst,
|
||||
useDeleteDesignCnstnBizScl,
|
||||
useDeleteDesignCnstnEsntlAtchFile,
|
||||
useDeleteDesignCnstnMng,
|
||||
useDeleteDesignCnstnRvwOpnn,
|
||||
useDesignCnstnBizCst,
|
||||
useDesignCnstnBizCstPage,
|
||||
useDesignCnstnBizScl,
|
||||
useDesignCnstnBizSclPage,
|
||||
useDesignCnstnEsntlAtchFile,
|
||||
useDesignCnstnEsntlAtchFilePage,
|
||||
useDesignCnstnMng,
|
||||
useDesignCnstnMngPage,
|
||||
useDesignCnstnRvwOpnn,
|
||||
useDesignCnstnRvwOpnnPage,
|
||||
useSaveApplyDesignCnstnMng,
|
||||
useSaveDesignCnstnBizCst,
|
||||
useSaveDesignCnstnBizScl,
|
||||
useSaveDesignCnstnEsntlAtchFile,
|
||||
useSaveDesignCnstnMng,
|
||||
useSaveDesignCnstnRvwOpnn,
|
||||
useTechReviewDesignCnstnMng,
|
||||
useUpdateDesignCnstnBizCst,
|
||||
useUpdateDesignCnstnBizScl,
|
||||
useUpdateDesignCnstnEsntlAtchFile,
|
||||
useUpdateDesignCnstnMng,
|
||||
useUpdateDesignCnstnRvwOpnn,
|
||||
} from "./api/queries";
|
||||
export type {
|
||||
DesignCnstnBizCstId,
|
||||
DesignCnstnBizCstResponseDto,
|
||||
DesignCnstnBizCstSaveRequestDto,
|
||||
DesignCnstnBizCstSearchDto,
|
||||
DesignCnstnBizCstService,
|
||||
DesignCnstnBizCstUpdateRequestDto,
|
||||
} from "./model/design-cnstn-biz-cst-interface";
|
||||
export type {
|
||||
DesignCnstnBizSclId,
|
||||
DesignCnstnBizSclResponseDto,
|
||||
DesignCnstnBizSclSaveRequestDto,
|
||||
DesignCnstnBizSclSearchDto,
|
||||
DesignCnstnBizSclService,
|
||||
DesignCnstnBizSclUpdateRequestDto,
|
||||
} from "./model/design-cnstn-biz-scl-interface";
|
||||
export type {
|
||||
DesignCnstnEsntlAtchFileId,
|
||||
DesignCnstnEsntlAtchFileResponseDto,
|
||||
DesignCnstnEsntlAtchFileSaveRequestDto,
|
||||
DesignCnstnEsntlAtchFileSearchDto,
|
||||
DesignCnstnEsntlAtchFileService,
|
||||
DesignCnstnEsntlAtchFileUpdateRequestDto,
|
||||
} from "./model/design-cnstn-esntl-atch-file-interface";
|
||||
export type {
|
||||
AtflMngClientResponseDto,
|
||||
BizCstItem,
|
||||
BizCstTreeDto,
|
||||
BizSclTreeDto,
|
||||
BzScaleItem,
|
||||
DesignCnstnApplySaveRequestDto,
|
||||
DesignCnstnMngDetResponseDto,
|
||||
DesignCnstnMngResponseDto,
|
||||
DesignCnstnMngSaveRequestDto,
|
||||
DesignCnstnMngSearchDto,
|
||||
DesignCnstnMngService,
|
||||
DesignCnstnMngUpdateRequestDto,
|
||||
DesignCnstnRvwOpnnResponseDto,
|
||||
DesignCnstnRvwOpnnSaveRequestDto,
|
||||
ElaprDocPrgrsClientResponseDto,
|
||||
EsntlAtchFileItem,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
} from "./model/design-cnstn-mng-interface";
|
||||
export type {
|
||||
DesignCnstnRvwOpnnId,
|
||||
DesignCnstnRvwOpnnSearchDto,
|
||||
DesignCnstnRvwOpnnService,
|
||||
DesignCnstnRvwOpnnUpdateRequestDto,
|
||||
} from "./model/design-cnstn-rvw-opnn-interface";
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { PageableParams, PageResponse } from "@/shared/model";
|
||||
|
||||
/** 백엔드 DesignCnstnBizCstId */
|
||||
export interface DesignCnstnBizCstId {
|
||||
designCnstnMng: string;
|
||||
acntgSeCd: string;
|
||||
}
|
||||
|
||||
/** 백엔드 DesignCnstnBizCstSaveRequestDto */
|
||||
export interface DesignCnstnBizCstSaveRequestDto {
|
||||
designCnstnDocNo: string;
|
||||
acntgSeCd: string;
|
||||
bscPlanAmt?: number;
|
||||
bgtRfltAmt?: number;
|
||||
tstmDesignAmt?: number;
|
||||
}
|
||||
|
||||
/** 백엔드 DesignCnstnBizCstUpdateRequestDto */
|
||||
export interface DesignCnstnBizCstUpdateRequestDto {
|
||||
bscPlanAmt?: number;
|
||||
bgtRfltAmt?: number;
|
||||
tstmDesignAmt?: number;
|
||||
}
|
||||
|
||||
/** 백엔드 DesignCnstnBizCstSearchDto + Pageable */
|
||||
export interface DesignCnstnBizCstSearchDto extends PageableParams {
|
||||
designCnstnDocNo?: string;
|
||||
acntgSeCd?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 DesignCnstnBizCstResponseDto */
|
||||
export interface DesignCnstnBizCstResponseDto {
|
||||
designCnstnDocNo: string;
|
||||
acntgSeCd: string;
|
||||
bscPlanAmt?: number;
|
||||
bgtRfltAmt?: number;
|
||||
tstmDesignAmt?: number;
|
||||
regDt?: string;
|
||||
}
|
||||
|
||||
/** Controller public 메서드명·시그니처와 동일 */
|
||||
export interface DesignCnstnBizCstService {
|
||||
findPage(
|
||||
searchDto: DesignCnstnBizCstSearchDto,
|
||||
): Promise<PageResponse<DesignCnstnBizCstResponseDto>>;
|
||||
findById(designCnstnDocNo: string, acntgSeCd: string): Promise<DesignCnstnBizCstResponseDto>;
|
||||
save(body: DesignCnstnBizCstSaveRequestDto): Promise<DesignCnstnBizCstId>;
|
||||
update(
|
||||
designCnstnDocNo: string,
|
||||
acntgSeCd: string,
|
||||
body: DesignCnstnBizCstUpdateRequestDto,
|
||||
): Promise<void>;
|
||||
delete(designCnstnDocNo: string, acntgSeCd: string): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { PageableParams, PageResponse } from "@/shared/model";
|
||||
|
||||
/** 백엔드 DesignCnstnBizSclId */
|
||||
export interface DesignCnstnBizSclId {
|
||||
designCnstnMng: string;
|
||||
cnstpSeCd: string;
|
||||
}
|
||||
|
||||
/** 백엔드 DesignCnstnBizSclSaveRequestDto */
|
||||
export interface DesignCnstnBizSclSaveRequestDto {
|
||||
designCnstnDocNo: string;
|
||||
cnstpSeCd: string;
|
||||
bscPlanQty?: number;
|
||||
bgtRfltQty?: number;
|
||||
tstmDesignQty?: number;
|
||||
}
|
||||
|
||||
/** 백엔드 DesignCnstnBizSclUpdateRequestDto */
|
||||
export interface DesignCnstnBizSclUpdateRequestDto {
|
||||
bscPlanQty?: number;
|
||||
bgtRfltQty?: number;
|
||||
tstmDesignQty?: number;
|
||||
}
|
||||
|
||||
/** 백엔드 DesignCnstnBizSclSearchDto + Pageable */
|
||||
export interface DesignCnstnBizSclSearchDto extends PageableParams {
|
||||
designCnstnDocNo?: string;
|
||||
cnstpSeCd?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 DesignCnstnBizSclResponseDto */
|
||||
export interface DesignCnstnBizSclResponseDto {
|
||||
designCnstnDocNo: string;
|
||||
cnstpSeCd: string;
|
||||
bscPlanQty?: number;
|
||||
bgtRfltQty?: number;
|
||||
tstmDesignQty?: number;
|
||||
regDt?: string;
|
||||
}
|
||||
|
||||
/** Controller public 메서드명·시그니처와 동일 */
|
||||
export interface DesignCnstnBizSclService {
|
||||
findPage(
|
||||
searchDto: DesignCnstnBizSclSearchDto,
|
||||
): Promise<PageResponse<DesignCnstnBizSclResponseDto>>;
|
||||
findById(designCnstnDocNo: string, cnstpSeCd: string): Promise<DesignCnstnBizSclResponseDto>;
|
||||
save(body: DesignCnstnBizSclSaveRequestDto): Promise<DesignCnstnBizSclId>;
|
||||
update(
|
||||
designCnstnDocNo: string,
|
||||
cnstpSeCd: string,
|
||||
body: DesignCnstnBizSclUpdateRequestDto,
|
||||
): Promise<void>;
|
||||
delete(designCnstnDocNo: string, cnstpSeCd: string): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { PageableParams, PageResponse } from "@/shared/model";
|
||||
|
||||
/** 백엔드 DesignCnstnEsntlAtchFileId */
|
||||
export interface DesignCnstnEsntlAtchFileId {
|
||||
designCnstnMng: string;
|
||||
esntlAtchFileId: string;
|
||||
}
|
||||
|
||||
/** 백엔드 DesignCnstnEsntlAtchFileSaveRequestDto */
|
||||
export interface DesignCnstnEsntlAtchFileSaveRequestDto {
|
||||
designCnstnDocNo: string;
|
||||
esntlAtchFileId: string;
|
||||
atchFileGroupId?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 DesignCnstnEsntlAtchFileUpdateRequestDto */
|
||||
export interface DesignCnstnEsntlAtchFileUpdateRequestDto {
|
||||
atchFileGroupId?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 DesignCnstnEsntlAtchFileSearchDto + Pageable */
|
||||
export interface DesignCnstnEsntlAtchFileSearchDto extends PageableParams {
|
||||
designCnstnDocNo?: string;
|
||||
esntlAtchFileId?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 DesignCnstnEsntlAtchFileResponseDto */
|
||||
export interface DesignCnstnEsntlAtchFileResponseDto {
|
||||
designCnstnDocNo: string;
|
||||
esntlAtchFileId: string;
|
||||
atchFileGroupId?: string;
|
||||
regDt?: string;
|
||||
}
|
||||
|
||||
/** Controller public 메서드명·시그니처와 동일 */
|
||||
export interface DesignCnstnEsntlAtchFileService {
|
||||
findPage(
|
||||
searchDto: DesignCnstnEsntlAtchFileSearchDto,
|
||||
): Promise<PageResponse<DesignCnstnEsntlAtchFileResponseDto>>;
|
||||
findById(
|
||||
designCnstnDocNo: string,
|
||||
esntlAtchFileId: string,
|
||||
): Promise<DesignCnstnEsntlAtchFileResponseDto>;
|
||||
save(body: DesignCnstnEsntlAtchFileSaveRequestDto): Promise<DesignCnstnEsntlAtchFileId>;
|
||||
update(
|
||||
designCnstnDocNo: string,
|
||||
esntlAtchFileId: string,
|
||||
body: DesignCnstnEsntlAtchFileUpdateRequestDto,
|
||||
): Promise<void>;
|
||||
delete(designCnstnDocNo: string, esntlAtchFileId: string): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import type {
|
||||
BizCstSaveBase,
|
||||
BzScaleSaveBase,
|
||||
EsntlAtchFileSaveBase,
|
||||
PageableParams,
|
||||
PageResponse,
|
||||
PbadmsApplySaveHeader,
|
||||
PbadmsMngDetResponseBase,
|
||||
} from "@/shared/model";
|
||||
|
||||
import type {
|
||||
DesignCnstnRvwOpnnResponseDto,
|
||||
DesignCnstnRvwOpnnSaveRequestDto,
|
||||
} from "./design-cnstn-rvw-opnn-interface";
|
||||
|
||||
export type {
|
||||
DesignCnstnRvwOpnnResponseDto,
|
||||
DesignCnstnRvwOpnnSaveRequestDto,
|
||||
} from "./design-cnstn-rvw-opnn-interface";
|
||||
export type {
|
||||
AtflMngClientResponseDto,
|
||||
BizCstTreeDto,
|
||||
BizSclTreeDto,
|
||||
ElaprDocPrgrsClientResponseDto,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
} from "@/shared/model";
|
||||
|
||||
/** 백엔드 DesignCnstnMngSaveRequestDto */
|
||||
export interface DesignCnstnMngSaveRequestDto {
|
||||
designCnstnDocNo?: string;
|
||||
admdstCd?: string;
|
||||
envoSeComCd?: string;
|
||||
taskSeComCd?: string;
|
||||
bizNm?: string;
|
||||
bizBgngYr?: string;
|
||||
bizEndYr?: string;
|
||||
bizCn?: string;
|
||||
aplcntNm?: string;
|
||||
aplcntMblTelno?: string;
|
||||
aplcntOgdpNm?: string;
|
||||
aplcntEmlAddr?: string;
|
||||
dsgnrNm?: string;
|
||||
dsgnrTelno?: string;
|
||||
dsgnrOgdpNm?: string;
|
||||
dsgnrEmlAddr?: string;
|
||||
atchFileGroupId?: string;
|
||||
prgrsSttsComCd?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 DesignCnstnMngUpdateRequestDto */
|
||||
export type DesignCnstnMngUpdateRequestDto = Omit<DesignCnstnMngSaveRequestDto, "designCnstnDocNo">;
|
||||
|
||||
/** 백엔드 DesignCnstnMngSearchDto + Pageable */
|
||||
export interface DesignCnstnMngSearchDto extends PageableParams {
|
||||
designCnstnDocNo?: string;
|
||||
admdstCd?: string;
|
||||
envoSeComCd?: string;
|
||||
taskSeComCd?: string;
|
||||
bizNm?: string;
|
||||
bizBgngYr?: string;
|
||||
bizEndYr?: string;
|
||||
bizCn?: string;
|
||||
aplcntNm?: string;
|
||||
aplcntMblTelno?: string;
|
||||
aplcntOgdpNm?: string;
|
||||
aplcntEmlAddr?: string;
|
||||
dsgnrNm?: string;
|
||||
dsgnrTelno?: string;
|
||||
dsgnrOgdpNm?: string;
|
||||
dsgnrEmlAddr?: string;
|
||||
atchFileGroupId?: string;
|
||||
}
|
||||
|
||||
interface DesignCnstnMngResponseExtension {
|
||||
designCnstnDocNo: string;
|
||||
envoSeCdNm?: string;
|
||||
taskSeCdNm?: string;
|
||||
prgrsSttsCdNm?: string;
|
||||
regDt?: string;
|
||||
rgtr?: string;
|
||||
rgtrNm?: string;
|
||||
mdfcnDt?: string;
|
||||
sido?: string;
|
||||
gugun?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 DesignCnstnMngResponseDto */
|
||||
export type DesignCnstnMngResponseDto = Omit<DesignCnstnMngSaveRequestDto, "designCnstnDocNo"> &
|
||||
DesignCnstnMngResponseExtension;
|
||||
|
||||
/** 백엔드 DesignCnstnApplySaveRequestDto.BzScaleItem */
|
||||
export type BzScaleItem = BzScaleSaveBase;
|
||||
|
||||
/** 백엔드 DesignCnstnApplySaveRequestDto.BizCstItem */
|
||||
export type BizCstItem = BizCstSaveBase;
|
||||
|
||||
/** 백엔드 DesignCnstnApplySaveRequestDto.EsntlAtchFileItem */
|
||||
export type EsntlAtchFileItem = EsntlAtchFileSaveBase;
|
||||
|
||||
/** 백엔드 DesignCnstnMngDetResponseDto */
|
||||
export type DesignCnstnMngDetResponseDto = PbadmsMngDetResponseBase<
|
||||
DesignCnstnMngResponseDto,
|
||||
DesignCnstnRvwOpnnResponseDto
|
||||
>;
|
||||
|
||||
/** 백엔드 DesignCnstnApplySaveRequestDto */
|
||||
export interface DesignCnstnApplySaveRequestDto extends PbadmsApplySaveHeader {
|
||||
designCnstnMng: DesignCnstnMngSaveRequestDto;
|
||||
bzScaleList?: BzScaleItem[];
|
||||
bizCstList?: BizCstItem[];
|
||||
esntlAtchFileList?: EsntlAtchFileItem[];
|
||||
rvwOpnnList?: DesignCnstnRvwOpnnSaveRequestDto[];
|
||||
}
|
||||
|
||||
/** Controller public 메서드명·시그니처와 동일 */
|
||||
export interface DesignCnstnMngService {
|
||||
findPage(searchDto: DesignCnstnMngSearchDto): Promise<PageResponse<DesignCnstnMngResponseDto>>;
|
||||
findById(id: string): Promise<DesignCnstnMngDetResponseDto>;
|
||||
save(body: DesignCnstnMngSaveRequestDto): Promise<string>;
|
||||
saveApply(body: DesignCnstnApplySaveRequestDto): Promise<string>;
|
||||
update(id: string, body: DesignCnstnMngUpdateRequestDto): Promise<void>;
|
||||
techReview(id: string, body: DesignCnstnApplySaveRequestDto): Promise<void>;
|
||||
delete(id: string): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { PageableParams, PageResponse, RvwOpnnResponseExtension } from "@/shared/model";
|
||||
|
||||
/** 백엔드 DesignCnstnRvwOpnnId */
|
||||
export interface DesignCnstnRvwOpnnId {
|
||||
designCnstnMng: string;
|
||||
rvwOpnnSn: number;
|
||||
}
|
||||
|
||||
/** 백엔드 DesignCnstnRvwOpnnSaveRequestDto */
|
||||
export interface DesignCnstnRvwOpnnSaveRequestDto {
|
||||
designCnstnDocNo: string;
|
||||
rvwOpnnSn?: number;
|
||||
opnrSeComCd?: string;
|
||||
opnrNm?: string;
|
||||
rvwRsltCn?: string;
|
||||
rvwRsltAnmlyYn?: string;
|
||||
atchFileGroupId?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 DesignCnstnRvwOpnnUpdateRequestDto */
|
||||
export interface DesignCnstnRvwOpnnUpdateRequestDto {
|
||||
opnrSeComCd?: string;
|
||||
opnrNm?: string;
|
||||
rvwRsltCn?: string;
|
||||
rvwRsltAnmlyYn?: string;
|
||||
atchFileGroupId?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 DesignCnstnRvwOpnnSearchDto + Pageable */
|
||||
export interface DesignCnstnRvwOpnnSearchDto extends PageableParams {
|
||||
designCnstnDocNo?: string;
|
||||
opnrNm?: string;
|
||||
opnrSeComCd?: string;
|
||||
rvwRsltCn?: string;
|
||||
rvwRsltAnmlyYn?: string;
|
||||
atchFileGroupId?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 DesignCnstnRvwOpnnResponseDto */
|
||||
export type DesignCnstnRvwOpnnResponseDto = Partial<
|
||||
Pick<DesignCnstnRvwOpnnSaveRequestDto, "designCnstnDocNo" | "rvwOpnnSn">
|
||||
> &
|
||||
Omit<DesignCnstnRvwOpnnSaveRequestDto, "designCnstnDocNo" | "rvwOpnnSn"> &
|
||||
RvwOpnnResponseExtension;
|
||||
|
||||
/** Controller public 메서드명·시그니처와 동일 */
|
||||
export interface DesignCnstnRvwOpnnService {
|
||||
findPage(
|
||||
searchDto: DesignCnstnRvwOpnnSearchDto,
|
||||
): Promise<PageResponse<DesignCnstnRvwOpnnResponseDto>>;
|
||||
findById(designCnstnDocNo: string, rvwOpnnSn: number): Promise<DesignCnstnRvwOpnnResponseDto>;
|
||||
save(body: DesignCnstnRvwOpnnSaveRequestDto): Promise<DesignCnstnRvwOpnnId>;
|
||||
update(
|
||||
designCnstnDocNo: string,
|
||||
rvwOpnnSn: number,
|
||||
body: DesignCnstnRvwOpnnUpdateRequestDto,
|
||||
): Promise<void>;
|
||||
delete(designCnstnDocNo: string, rvwOpnnSn: number): Promise<void>;
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+5
-4
@@ -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<string, ApproverStatusConfig> = {
|
||||
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(
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 */
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+8
-12
@@ -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({
|
||||
<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>
|
||||
+22
-33
@@ -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<string | null>(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({
|
||||
>
|
||||
<Table className="w-full">
|
||||
<colgroup>
|
||||
<col width="8%" />
|
||||
<col width="8%" />
|
||||
<col width="10%" />
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
<col width="12%" />
|
||||
<col width="20%" />
|
||||
<col width="30%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="24%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -326,6 +323,7 @@ function ElaprApprovalDialog({
|
||||
<TableHead>결재순서</TableHead>
|
||||
<TableHead>결재구분</TableHead>
|
||||
<TableHead>결재자</TableHead>
|
||||
<TableHead>전결자 지정</TableHead>
|
||||
<TableHead>결재상태</TableHead>
|
||||
<TableHead>관리</TableHead>
|
||||
</TableRow>
|
||||
@@ -354,6 +352,7 @@ function ElaprApprovalDialog({
|
||||
setPickingRowKey(row.rowKey);
|
||||
setPickerOpen(true);
|
||||
}}
|
||||
onToggleDcarb={(checked) => setDcarbYn(row.rowKey, checked)}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
@@ -384,16 +383,6 @@ function ElaprApprovalDialog({
|
||||
</Button>
|
||||
{canApprove && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="mr-2 flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="all-approve"
|
||||
checked={finalApprovalYn}
|
||||
onCheckedChange={(checked) => setFinalApprovalYn(checked === true)}
|
||||
/>
|
||||
<Label htmlFor="all-approve" className="text-xl">
|
||||
전결
|
||||
</Label>
|
||||
</div>
|
||||
<Button onClick={handleApprove} disabled={isBusy}>
|
||||
결재
|
||||
</Button>
|
||||
@@ -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 = (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center rounded-sm px-2 py-1 text-lg font-medium",
|
||||
status.badgeClass,
|
||||
isRejected && rjctRsn && "cursor-help underline decoration-dotted underline-offset-4",
|
||||
)}
|
||||
>
|
||||
{status.label}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex min-w-[12rem] flex-1 flex-col items-center gap-3 p-4 text-center">
|
||||
{isRejected && rjctRsn ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{statusBadge}</TooltipTrigger>
|
||||
<TooltipContent sideOffset={4} className="max-w-md whitespace-pre-wrap">
|
||||
{rjctRsn}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
statusBadge
|
||||
)}
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl text-muted-foreground">[{item.atrzSeComNm || "-"}]</span>
|
||||
<span className="text-xl font-semibold">{item.aprvrNm || item.aprvrUuid || "-"}</span>
|
||||
</div>
|
||||
{metaParts.length > 0 && (
|
||||
<span className="text-base text-gray-500">{metaParts.join(" · ")}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ElaprDocPrgrsStatus({ elaprDocNo }: ElaprDocPrgrsStatusProps) {
|
||||
const { data, isLoading } = useElaprDocPrgrsByDocNo(elaprDocNo);
|
||||
const steps = sortByAtrzSeqNo(data ?? []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 px-4 py-6">
|
||||
<Skeleton className="h-24 flex-1 rounded-sm" />
|
||||
<Skeleton className="h-24 flex-1 rounded-sm" />
|
||||
<Skeleton className="h-24 flex-1 rounded-sm" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (steps.length === 0) {
|
||||
return <div className="py-8 text-center text-xl text-gray-500">결재 정보가 없습니다.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-stretch justify-start gap-4 overflow-x-auto">
|
||||
{steps.map((item, index) => (
|
||||
<Fragment key={item.elaprPrgrsId ?? `${item.atrzSeqNo}-${item.aprvrUuid ?? index}`}>
|
||||
<div className="flex items-center">
|
||||
{index > 0 && <ChevronRight className="size-8 text-muted-foreground" />}
|
||||
</div>
|
||||
<ElaprDocPrgrsStep item={item} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ElaprDocPrgrsStatus };
|
||||
+166
-163
@@ -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<ElaprTmplt | null>(null);
|
||||
const [aplnRows, setAplnRows] = useState<ElaprAplnRow[]>([]);
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="max-w-[120rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>결재선 선택</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex gap-6">
|
||||
<ElaprTmpltSelectTable
|
||||
query={tmpltQuery}
|
||||
selectedTmpltId={selectedTmplt?.elaprTmpltId ?? null}
|
||||
onSelectTmplt={handleSelectTmplt}
|
||||
/>
|
||||
<ElaprAplnPickerGrid
|
||||
elaprTmpltId={selectedTmplt?.elaprTmpltId ?? null}
|
||||
onRowsChange={handleRowsChange}
|
||||
onEditingLockedChange={handleEditingLockedChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" color="white" onClick={() => handleOpenChange(false)}>
|
||||
닫기
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={!selectedTmplt || isEditingLocked}
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
확인
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
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<ElaprTmplt | null>(null);
|
||||
const [aplnRows, setAplnRows] = useState<ElaprAplnRow[]>([]);
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="max-w-[120rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>결재선 선택</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex gap-6">
|
||||
<ElaprTmpltSelectTable
|
||||
query={tmpltQuery}
|
||||
selectedTmpltId={selectedTmplt?.elaprTmpltId ?? null}
|
||||
onSelectTmplt={handleSelectTmplt}
|
||||
/>
|
||||
<ElaprAplnPickerGrid
|
||||
elaprTmpltId={selectedTmplt?.elaprTmpltId ?? null}
|
||||
onRowsChange={handleRowsChange}
|
||||
onEditingLockedChange={handleEditingLockedChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" color="white" onClick={() => handleOpenChange(false)}>
|
||||
닫기
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={!selectedTmplt || isEditingLocked}
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
확인
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { ElaprTmpltPickerDialog };
|
||||
export type { ElaprTmpltPickerConfirmPayload };
|
||||
+2
-9
@@ -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<ElaprTmplt>();
|
||||
|
||||
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 ?? "—",
|
||||
}),
|
||||
],
|
||||
[],
|
||||
);
|
||||
+31
-16
@@ -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({
|
||||
<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>
|
||||
)}
|
||||
+46
-13
@@ -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<ElaprPrgrsSttsRow>) => 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({
|
||||
<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 || "-"
|
||||
)}
|
||||
@@ -123,6 +147,15 @@ function SortablePrgrsSttsRow({
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{isDcarbable && !rowDisabled && (
|
||||
<Checkbox
|
||||
checked={row.dcarbYn === "Y"}
|
||||
onCheckedChange={(checked) => onToggleDcarb(checked === true)}
|
||||
aria-label="전결자 지정"
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{isRejectedRow && rjctRsn ? (
|
||||
<Tooltip>
|
||||
@@ -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<string> {
|
||||
const response = await client.post<string>(`${BASE}/${id}/submitsupplement`, payload);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function submitReqSupplement(
|
||||
id: string,
|
||||
payload: InstlSncnApplySaveRequestDto,
|
||||
): Promise<string> {
|
||||
const response = await client.post<string>(`${BASE}/${id}/reqsupplement`, payload);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function remove(id: string): Promise<void> {
|
||||
await client.delete(`${BASE}/${id}`);
|
||||
}
|
||||
@@ -80,6 +96,8 @@ export const instlSncnService = {
|
||||
saveApply,
|
||||
update,
|
||||
techReview,
|
||||
submitSupplement,
|
||||
submitReqSupplement,
|
||||
remove,
|
||||
changePrgrsSttsComCd,
|
||||
} satisfies InstlSncnMngService;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<string, InstlSncnCklstResultSaveDto[]>;
|
||||
/** 필수첨부파일 목록 */
|
||||
esntlAtchFileList: InstlSncnEsntlAtchFileSaveDto[];
|
||||
esntlAtchFileList?: InstlSncnEsntlAtchFileSaveDto[];
|
||||
/** 검토의견 목록 */
|
||||
rvwOpnnList?: InstlSncnRvwOpnnSaveRequestDto[];
|
||||
/** 보완요구 목록 */
|
||||
@@ -222,8 +229,11 @@ export type InstlSncnRvwOpnnResponseDto = InstlSncnRvwOpnnSaveRequestDto &
|
||||
};
|
||||
|
||||
/** 설치인가관리 상세 응답 DTO */
|
||||
export interface InstlSncnMngDetResponse
|
||||
extends PbadmsMngDetResponseBase<InstlSncnMngResponseDto, InstlSncnRvwOpnnResponseDto> {
|
||||
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<string>;
|
||||
update: (id: string, payload: InstlSncnMngUpdateRequestDto) => Promise<void>;
|
||||
techReview: (id: string, payload: InstlSncnApplySaveRequestDto) => Promise<void>;
|
||||
submitSupplement: (id: string, payload: InstlSncnApplySaveRequestDto) => Promise<string>;
|
||||
submitReqSupplement: (id: string, payload: InstlSncnApplySaveRequestDto) => Promise<string>;
|
||||
remove: (id: string) => Promise<void>;
|
||||
changePrgrsSttsComCd: (payload: DocPrgrsUpdateRequestDto) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export { DesignEditPage } from "./ui/design-edit-page";
|
||||
export { DesignFormPage } from "./ui/design-form-page";
|
||||
export { DesignListPage } from "./ui/design-list-page";
|
||||
export { DesignRegistPage } from "./ui/design-regist-page";
|
||||
export { DesignViewPage } from "./ui/design-view-page";
|
||||
@@ -0,0 +1,118 @@
|
||||
export const DESIGN_CNSTN_VIEW_BUTTON_ACTION = {
|
||||
strg: "strg",
|
||||
aply: "aply",
|
||||
del: "del",
|
||||
rqstRvw: "rqstRvw",
|
||||
rvwStrg: "rvwStrg",
|
||||
rvwCmptn: "rvwCmptn",
|
||||
atrzAprv: "atrzAprv",
|
||||
atrzRply: "atrzRply",
|
||||
} as const;
|
||||
|
||||
export type DesignCnstnViewButtonAction =
|
||||
(typeof DESIGN_CNSTN_VIEW_BUTTON_ACTION)[keyof typeof DESIGN_CNSTN_VIEW_BUTTON_ACTION];
|
||||
|
||||
const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, DesignCnstnViewButtonAction[]>> = {
|
||||
ROLE_USER05: {
|
||||
"": [
|
||||
DESIGN_CNSTN_VIEW_BUTTON_ACTION.strg,
|
||||
DESIGN_CNSTN_VIEW_BUTTON_ACTION.aply,
|
||||
DESIGN_CNSTN_VIEW_BUTTON_ACTION.del,
|
||||
],
|
||||
CV: [
|
||||
DESIGN_CNSTN_VIEW_BUTTON_ACTION.strg,
|
||||
DESIGN_CNSTN_VIEW_BUTTON_ACTION.aply,
|
||||
DESIGN_CNSTN_VIEW_BUTTON_ACTION.del,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const ROLE_USER01_ATRZ_STATUS_CODES = ["CT", "CS"] as const;
|
||||
|
||||
const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record<
|
||||
string,
|
||||
Record<string, DesignCnstnViewButtonAction[]>
|
||||
> = {
|
||||
CT: {
|
||||
ATRZ01: [DESIGN_CNSTN_VIEW_BUTTON_ACTION.rqstRvw],
|
||||
},
|
||||
CS: {
|
||||
ATRZ02: [DESIGN_CNSTN_VIEW_BUTTON_ACTION.rvwStrg, DESIGN_CNSTN_VIEW_BUTTON_ACTION.rvwCmptn],
|
||||
ATRZ03: [DESIGN_CNSTN_VIEW_BUTTON_ACTION.atrzAprv],
|
||||
ATRZ99: [DESIGN_CNSTN_VIEW_BUTTON_ACTION.atrzRply],
|
||||
},
|
||||
};
|
||||
|
||||
function resolveButtonConfigs(
|
||||
actions: DesignCnstnViewButtonAction[],
|
||||
): DesignCnstnViewButtonAction[] {
|
||||
return actions.map((action) => DESIGN_CNSTN_VIEW_BUTTON_ACTION[action]);
|
||||
}
|
||||
|
||||
interface GetButtonConfigsOptions {
|
||||
hasPendingApprovalRequest?: boolean;
|
||||
}
|
||||
|
||||
function resolveRoleUser01AtrzButtonActions(
|
||||
prgrsSttsComCd: string,
|
||||
atrzSeComCdList: string[],
|
||||
options?: GetButtonConfigsOptions,
|
||||
): DesignCnstnViewButtonAction[] {
|
||||
if (prgrsSttsComCd === "CS" && options?.hasPendingApprovalRequest === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const atrzStatusMap = ROLE_USER01_ATRZ_STATUS_ACTIONS[prgrsSttsComCd];
|
||||
if (!atrzStatusMap || atrzSeComCdList.length === 0) return [];
|
||||
|
||||
const seen = new Set<DesignCnstnViewButtonAction>();
|
||||
const actions: DesignCnstnViewButtonAction[] = [];
|
||||
|
||||
for (const atrzSeComCd of atrzSeComCdList) {
|
||||
const mappedActions = atrzStatusMap[atrzSeComCd];
|
||||
if (!mappedActions) continue;
|
||||
|
||||
for (const action of mappedActions) {
|
||||
if (seen.has(action)) continue;
|
||||
seen.add(action);
|
||||
actions.push(action);
|
||||
}
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
export function needsRoleUser01AtrzResolve(
|
||||
dtlInstCd: string | undefined,
|
||||
prgrsSttsComCd: string,
|
||||
): boolean {
|
||||
return (
|
||||
dtlInstCd === "ROLE_USER01" &&
|
||||
ROLE_USER01_ATRZ_STATUS_CODES.includes(
|
||||
prgrsSttsComCd as (typeof ROLE_USER01_ATRZ_STATUS_CODES)[number],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function getButtonConfigs(
|
||||
dtlInstCd: string | undefined,
|
||||
prgrsSttsComCd: string,
|
||||
atrzSeComCdList?: string[],
|
||||
options?: GetButtonConfigsOptions,
|
||||
): DesignCnstnViewButtonAction[] {
|
||||
if (!dtlInstCd) return [];
|
||||
|
||||
if (needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd)) {
|
||||
return resolveButtonConfigs(
|
||||
resolveRoleUser01AtrzButtonActions(prgrsSttsComCd, atrzSeComCdList ?? [], options),
|
||||
);
|
||||
}
|
||||
|
||||
const statusMap = ROLE_STATUS_BUTTON_ACTIONS[dtlInstCd];
|
||||
if (!statusMap) return [];
|
||||
|
||||
const actions = statusMap[prgrsSttsComCd];
|
||||
if (!actions) return [];
|
||||
|
||||
return resolveButtonConfigs(actions);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const designCnstnRegistSchema = z.object({
|
||||
designCnstnMng: z.object({
|
||||
bizNm: z.string().min(1, "사업명은 필수입니다"),
|
||||
bizBgngYr: z.string().min(1, "사업시작연도는 필수입니다"),
|
||||
bizEndYr: z.string().min(1, "사업종료연도는 필수입니다"),
|
||||
bizCn: z.string().min(1, "사업내용은 필수입니다"),
|
||||
taskSeComCd: z.array(z.string()).min(1, "구분은 필수입니다"),
|
||||
dsgnrNm: z.string().min(1, "설계자 이름은 필수입니다"),
|
||||
dsgnrTelno: z
|
||||
.string()
|
||||
.min(1, "설계자 전화번호는 필수입니다")
|
||||
.regex(/^[0-9-]+$/, "숫자와 하이픈(-)만 입력 가능합니다"),
|
||||
dsgnrOgdpNm: z.string().min(1, "설계자 소속은 필수입니다"),
|
||||
dsgnrEmlAddr: z
|
||||
.string()
|
||||
.min(1, "설계자 이메일은 필수입니다")
|
||||
.email("올바른 이메일 형식이 아닙니다"),
|
||||
aplcntNm: z.string().optional(),
|
||||
aplcntMblTelno: z.string().optional(),
|
||||
aplcntOgdpNm: z.string().optional(),
|
||||
aplcntEmlAddr: z.string().optional(),
|
||||
designCnstnDocNo: z.string().optional(),
|
||||
admdstCd: z.string().optional(),
|
||||
envoSeComCd: z.string().optional(),
|
||||
prgrsSttsComCd: z.string().optional(),
|
||||
atchFileGroupId: z.string().optional(),
|
||||
envoSeCdNm: z.string().optional(),
|
||||
taskSeCdNm: z.string().optional(),
|
||||
prgrsSttsCdNm: z.string().optional(),
|
||||
sido: z.string().optional(),
|
||||
gugun: z.string().optional(),
|
||||
regDt: z.string().optional(),
|
||||
rgtr: z.string().optional(),
|
||||
rgtrNm: z.string().optional(),
|
||||
mdfcnDt: z.string().optional(),
|
||||
}),
|
||||
bzScaleList: z.array(
|
||||
z.object({
|
||||
cnstpSeCd: z.string(),
|
||||
bscPl: z.number(),
|
||||
budgetReflct: z.number(),
|
||||
thtiDsgn: z.number(),
|
||||
diff: z.number().optional(),
|
||||
}),
|
||||
),
|
||||
esntlAtchFileList: z.array(
|
||||
z.object({
|
||||
esntlAtchFileId: z.string(),
|
||||
atchFileGroupId: z.string(),
|
||||
}),
|
||||
),
|
||||
privacyConsent: z.enum(["Y", "N"], {
|
||||
required_error: "개인정보 수집/이용 동의는 필수입니다",
|
||||
}),
|
||||
});
|
||||
|
||||
export type DesignCnstnRegistFormData = z.infer<typeof designCnstnRegistSchema>;
|
||||
|
||||
export function formatTaskSeComCdForApi(taskSeComCd: string[]): string {
|
||||
return taskSeComCd.join(",");
|
||||
}
|
||||
|
||||
export function parseTaskSeComCdFromApi(taskSeComCd?: string): string[] {
|
||||
if (!taskSeComCd) return [];
|
||||
return taskSeComCd.split(",").filter(Boolean);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import type { IUserInfo } from "@/shared/store";
|
||||
import type { DesignCnstnMngDetResponseDto } from "@/entities/design-cnstn";
|
||||
|
||||
import type { DesignCnstnRegistFormData } from "./design-cnstn-regist-schema";
|
||||
import { parseTaskSeComCdFromApi } from "./design-cnstn-regist-schema";
|
||||
|
||||
function mapEsntlAtchFileList(esntlFileList: DesignCnstnMngDetResponseDto["esntlFileList"]) {
|
||||
const esntlMap = new Map<string, string>();
|
||||
|
||||
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 mapDetailToFormData(data: DesignCnstnMngDetResponseDto): DesignCnstnRegistFormData {
|
||||
const detDto = data.detDto;
|
||||
if (!detDto) {
|
||||
throw new Error("상세 정보(detDto)가 없습니다.");
|
||||
}
|
||||
|
||||
const bizSclList = data.bizSclList ?? [];
|
||||
|
||||
return {
|
||||
designCnstnMng: {
|
||||
designCnstnDocNo: detDto.designCnstnDocNo,
|
||||
bizNm: detDto.bizNm ?? "",
|
||||
bizBgngYr: detDto.bizBgngYr ?? "",
|
||||
bizEndYr: detDto.bizEndYr ?? "",
|
||||
bizCn: detDto.bizCn ?? "",
|
||||
taskSeComCd: parseTaskSeComCdFromApi(detDto.taskSeComCd),
|
||||
dsgnrNm: detDto.dsgnrNm ?? "",
|
||||
dsgnrTelno: detDto.dsgnrTelno ?? "",
|
||||
dsgnrOgdpNm: detDto.dsgnrOgdpNm ?? "",
|
||||
dsgnrEmlAddr: detDto.dsgnrEmlAddr ?? "",
|
||||
aplcntNm: detDto.aplcntNm ?? "",
|
||||
aplcntMblTelno: detDto.aplcntMblTelno ?? "",
|
||||
aplcntOgdpNm: detDto.aplcntOgdpNm ?? "",
|
||||
aplcntEmlAddr: detDto.aplcntEmlAddr ?? "",
|
||||
admdstCd: detDto.admdstCd,
|
||||
envoSeComCd: detDto.envoSeComCd,
|
||||
prgrsSttsComCd: detDto.prgrsSttsComCd,
|
||||
atchFileGroupId: detDto.atchFileGroupId ?? "",
|
||||
envoSeCdNm: detDto.envoSeCdNm,
|
||||
taskSeCdNm: detDto.taskSeCdNm,
|
||||
prgrsSttsCdNm: detDto.prgrsSttsCdNm,
|
||||
sido: detDto.sido,
|
||||
gugun: detDto.gugun,
|
||||
regDt: detDto.regDt,
|
||||
rgtr: detDto.rgtr,
|
||||
rgtrNm: detDto.rgtrNm,
|
||||
mdfcnDt: detDto.mdfcnDt,
|
||||
},
|
||||
bzScaleList: bizSclList.map((item) => ({
|
||||
cnstpSeCd: item.cnstpSeCd,
|
||||
bscPl: item.bscPlanQty,
|
||||
budgetReflct: item.bgtRfltQty,
|
||||
thtiDsgn: item.tstmDesignQty,
|
||||
diff: item.tstmDesignQty - item.bgtRfltQty,
|
||||
})),
|
||||
esntlAtchFileList: mapEsntlAtchFileList(data.esntlFileList),
|
||||
privacyConsent: "Y",
|
||||
};
|
||||
}
|
||||
|
||||
export function getCreateDefaultValues(
|
||||
userInfo: IUserInfo | null,
|
||||
taskSeComCd?: string[],
|
||||
): DesignCnstnRegistFormData {
|
||||
return {
|
||||
designCnstnMng: {
|
||||
bizNm: "",
|
||||
bizBgngYr: "",
|
||||
bizEndYr: "",
|
||||
bizCn: "",
|
||||
taskSeComCd: taskSeComCd ?? [],
|
||||
dsgnrNm: "",
|
||||
dsgnrTelno: "",
|
||||
dsgnrOgdpNm: "",
|
||||
dsgnrEmlAddr: "",
|
||||
aplcntNm: userInfo?.userName ?? "",
|
||||
aplcntMblTelno: userInfo?.mphn ?? "",
|
||||
aplcntOgdpNm: userInfo?.ogdpSeNm ?? "",
|
||||
aplcntEmlAddr: userInfo?.email ?? "",
|
||||
prgrsSttsComCd: "CV",
|
||||
atchFileGroupId: "",
|
||||
},
|
||||
bzScaleList: [],
|
||||
esntlAtchFileList: [],
|
||||
privacyConsent: "N",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import type {
|
||||
DesignCnstnMngDetResponseDto,
|
||||
DesignCnstnRvwOpnnResponseDto,
|
||||
DesignCnstnRvwOpnnSaveRequestDto,
|
||||
} from "@/entities/design-cnstn";
|
||||
|
||||
export interface ReviewOpinionFormData {
|
||||
rvwRsltCn: string;
|
||||
rvwRsltAnmlyYn: "Y" | "N" | "";
|
||||
atchFileGroupId: string;
|
||||
opnrSeComCd: string;
|
||||
rvwOpnnSn?: number;
|
||||
}
|
||||
|
||||
export interface ReviewOpinionGetFormDataResult {
|
||||
formData: ReviewOpinionFormData;
|
||||
rvwList: DesignCnstnRvwOpnnResponseDto[];
|
||||
}
|
||||
|
||||
function hasReviewOpinionContent(formData: ReviewOpinionFormData) {
|
||||
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
||||
}
|
||||
|
||||
function mapServerItemToSaveDto(
|
||||
item: DesignCnstnRvwOpnnResponseDto,
|
||||
designCnstnDocNo: string,
|
||||
): DesignCnstnRvwOpnnSaveRequestDto | null {
|
||||
if (item.rvwOpnnSn == null) return null;
|
||||
|
||||
return {
|
||||
designCnstnDocNo,
|
||||
rvwOpnnSn: item.rvwOpnnSn,
|
||||
opnrSeComCd: item.opnrSeComCd,
|
||||
opnrNm: item.opnrNm,
|
||||
rvwRsltCn: item.rvwRsltCn,
|
||||
rvwRsltAnmlyYn: item.rvwRsltAnmlyYn || undefined,
|
||||
atchFileGroupId: item.atchFileGroupId || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function mapFormDataToSaveDto(
|
||||
formData: ReviewOpinionFormData,
|
||||
designCnstnDocNo: string,
|
||||
opnrNm?: string,
|
||||
): DesignCnstnRvwOpnnSaveRequestDto {
|
||||
const base = {
|
||||
designCnstnDocNo,
|
||||
opnrSeComCd: formData.opnrSeComCd,
|
||||
opnrNm: opnrNm ?? "",
|
||||
rvwRsltCn: formData.rvwRsltCn,
|
||||
rvwRsltAnmlyYn: formData.rvwRsltAnmlyYn || undefined,
|
||||
atchFileGroupId: formData.atchFileGroupId || undefined,
|
||||
};
|
||||
|
||||
if (formData.rvwOpnnSn != null) {
|
||||
return { ...base, rvwOpnnSn: formData.rvwOpnnSn };
|
||||
}
|
||||
|
||||
return base as DesignCnstnRvwOpnnSaveRequestDto;
|
||||
}
|
||||
|
||||
export function toRvwOpnnSaveDto(
|
||||
data: DesignCnstnMngDetResponseDto,
|
||||
result: ReviewOpinionGetFormDataResult | null | undefined,
|
||||
opnrNm?: string,
|
||||
): DesignCnstnRvwOpnnSaveRequestDto[] | undefined {
|
||||
if (!result || !data.detDto) return undefined;
|
||||
|
||||
const { formData, rvwList } = result;
|
||||
const designCnstnDocNo = data.detDto.designCnstnDocNo;
|
||||
|
||||
const list: DesignCnstnRvwOpnnSaveRequestDto[] = rvwList
|
||||
.map((item) => mapServerItemToSaveDto(item, designCnstnDocNo))
|
||||
.filter((item): item is DesignCnstnRvwOpnnSaveRequestDto => item != null);
|
||||
|
||||
if (!hasReviewOpinionContent(formData)) {
|
||||
return list.length > 0 ? list : undefined;
|
||||
}
|
||||
|
||||
const formDto = mapFormDataToSaveDto(formData, designCnstnDocNo, opnrNm);
|
||||
|
||||
if (formData.rvwOpnnSn != null) {
|
||||
const existingIndex = list.findIndex((item) => item.rvwOpnnSn === formData.rvwOpnnSn);
|
||||
if (existingIndex >= 0) {
|
||||
list[existingIndex] = formDto;
|
||||
} else {
|
||||
list.push(formDto);
|
||||
}
|
||||
} else {
|
||||
list.push(formDto);
|
||||
}
|
||||
|
||||
return list.length > 0 ? list : undefined;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { ReviewStepCode } from "../model/view-types";
|
||||
|
||||
export function getEditableReviewStep(
|
||||
dtlInstCd?: string,
|
||||
prgrsSttsComCd?: string,
|
||||
): ReviewStepCode | undefined {
|
||||
if (dtlInstCd === "ROLE_USER01" && prgrsSttsComCd === "CS") {
|
||||
return "KECO";
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import type {
|
||||
BizSclTreeDto,
|
||||
BzScaleItem,
|
||||
DesignCnstnApplySaveRequestDto,
|
||||
DesignCnstnMngDetResponseDto,
|
||||
EsntlAtchFileItem,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
} from "@/entities/design-cnstn";
|
||||
|
||||
import type { ReviewStepCode } from "../model/view-types";
|
||||
|
||||
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 toEsntlAtchFileSaveDto(
|
||||
esntlFileList: EsntlAtchFileMngTreeResponseDto[],
|
||||
): EsntlAtchFileItem[] {
|
||||
const esntlMap = new Map<string, string>();
|
||||
|
||||
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: DesignCnstnMngDetResponseDto,
|
||||
prgrsSttsComCd: string,
|
||||
): DesignCnstnApplySaveRequestDto {
|
||||
const detDto = data.detDto;
|
||||
if (!detDto) {
|
||||
throw new Error("상세 정보(detDto)가 없습니다.");
|
||||
}
|
||||
|
||||
return {
|
||||
designCnstnMng: { ...detDto, prgrsSttsComCd },
|
||||
bzScaleList: toBzScaleSaveDto(data.bizSclList ?? []),
|
||||
esntlAtchFileList: toEsntlAtchFileSaveDto(data.esntlFileList ?? []),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTechReviewPayload(
|
||||
data: DesignCnstnMngDetResponseDto,
|
||||
prgrsSttsComCd: string,
|
||||
techReViewSaveCd: string,
|
||||
reviewStep: ReviewStepCode,
|
||||
reviewOpinionFormData?: ReviewOpinionGetFormDataResult | null,
|
||||
opnrNm?: string,
|
||||
): DesignCnstnApplySaveRequestDto {
|
||||
return {
|
||||
...buildSavePayload(data, prgrsSttsComCd),
|
||||
techReViewSaveCd,
|
||||
rvwStpCd: reviewStep,
|
||||
rvwOpnnList: toRvwOpnnSaveDto(data, reviewOpinionFormData, opnrNm),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
|
||||
import { dayjs } from "@/shared/lib";
|
||||
import type { DesignCnstnMngResponseDto } from "@/entities/design-cnstn";
|
||||
import { Badge } from "@common/components";
|
||||
|
||||
const columnHelper = createColumnHelper<DesignCnstnMngResponseDto>();
|
||||
|
||||
const MAIN_STEP_LABELS = {
|
||||
save: "저장",
|
||||
apply: "신청완료",
|
||||
review: "설계자문 검토",
|
||||
reply: "설계검토 회신",
|
||||
} as const;
|
||||
|
||||
const REVIEW_STATUS_CODES = new Set(["CS", "CR"]);
|
||||
|
||||
type BadgeState = "completed" | "current" | "pending" | "action";
|
||||
|
||||
interface ProgressBadge {
|
||||
label: string;
|
||||
state: BadgeState;
|
||||
}
|
||||
|
||||
function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): ProgressBadge[] {
|
||||
const reviewLabel = prgrsSttsCdNm || MAIN_STEP_LABELS.review;
|
||||
const replyLabel = prgrsSttsCdNm || MAIN_STEP_LABELS.reply;
|
||||
|
||||
if (prgrsSttsComCd === "CV") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.save, state: "current" },
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.review, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "CT") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.save, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "current" },
|
||||
{ label: MAIN_STEP_LABELS.review, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (REVIEW_STATUS_CODES.has(prgrsSttsComCd)) {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.save, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: reviewLabel, state: "action" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "CP" || prgrsSttsComCd === "EP") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.save, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.review, state: "completed" },
|
||||
{ label: replyLabel, state: "action" },
|
||||
];
|
||||
}
|
||||
|
||||
return [{ label: prgrsSttsCdNm || prgrsSttsComCd || "-", state: "current" }];
|
||||
}
|
||||
|
||||
function getBadgeColor(state: BadgeState) {
|
||||
switch (state) {
|
||||
case "completed":
|
||||
return "green";
|
||||
case "current":
|
||||
return "blue";
|
||||
case "pending":
|
||||
return "lightGray";
|
||||
case "action":
|
||||
return "orange";
|
||||
}
|
||||
}
|
||||
|
||||
function formatOptional(value: string | undefined) {
|
||||
return value?.trim() ? value : "-";
|
||||
}
|
||||
|
||||
interface ProgressStepBadgeProps {
|
||||
prgrsSttsComCd: string;
|
||||
prgrsSttsCdNm: string;
|
||||
}
|
||||
|
||||
function ProgressStepBadge({ prgrsSttsComCd, prgrsSttsCdNm }: ProgressStepBadgeProps) {
|
||||
const badges = buildProgressBadges(prgrsSttsComCd, prgrsSttsCdNm);
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-start">
|
||||
{badges.map((badge, index) => (
|
||||
<Fragment key={`${badge.label}-${index}`}>
|
||||
{index > 0 && <span className="mx-2">></span>}
|
||||
<Badge color={getBadgeColor(badge.state)}>{badge.label}</Badge>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function createDesignCnstnColumns() {
|
||||
return [
|
||||
columnHelper.accessor("sido", {
|
||||
header: "시도",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("gugun", {
|
||||
header: "시군구",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("bizNm", {
|
||||
header: "사업명",
|
||||
meta: { align: "left" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("taskSeCdNm", {
|
||||
header: "업무구분",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
|
||||
}),
|
||||
columnHelper.accessor("aplcntNm", {
|
||||
header: "담당자",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("envoSeCdNm", {
|
||||
header: "해당기관",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "progress",
|
||||
header: "진행상황",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) => (
|
||||
<ProgressStepBadge
|
||||
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
|
||||
prgrsSttsCdNm={row.original.prgrsSttsCdNm || ""}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("regDt", {
|
||||
header: "신청일",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue();
|
||||
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export type ReviewStepCode = "LOCAL" | "ENV" | "KECO";
|
||||
|
||||
export const REVIEW_STEP_LABELS: Record<ReviewStepCode, string> = {
|
||||
LOCAL: "지자체",
|
||||
ENV: "환경청",
|
||||
KECO: "환경공단",
|
||||
};
|
||||
|
||||
export const DESIGN_PROGRESS_ORDER = ["CV", "CT", "CS", "CR", "CP", "EP"] as const;
|
||||
|
||||
export const ROLE_TO_REVIEW_STEP: Record<string, ReviewStepCode> = {
|
||||
ROLE_USER05: "LOCAL",
|
||||
ROLE_USER03: "ENV",
|
||||
ROLE_USER01: "KECO",
|
||||
};
|
||||
@@ -0,0 +1,391 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import {
|
||||
type DesignCnstnApplySaveRequestDto,
|
||||
designCnstnMngBaseKey,
|
||||
type DesignCnstnMngDetResponseDto,
|
||||
designCnstnMngService,
|
||||
} from "@/entities/design-cnstn";
|
||||
import {
|
||||
buildElaprPrgrsSttsListFromAplnRows,
|
||||
ElaprApprovalDialog,
|
||||
type ElaprDmndSaveRequestDto,
|
||||
elaprDmndService,
|
||||
type ElaprTmpltPickerConfirmPayload,
|
||||
ElaprTmpltPickerDialog,
|
||||
hasCurrentUserPendingApprovalRequest,
|
||||
useElaprDocPrgrsByDocNo,
|
||||
} from "@/entities/elapr";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import {
|
||||
DESIGN_CNSTN_VIEW_BUTTON_ACTION,
|
||||
type DesignCnstnViewButtonAction,
|
||||
getButtonConfigs,
|
||||
needsRoleUser01AtrzResolve,
|
||||
} from "../../lib/button-config";
|
||||
import { buildSavePayload, buildTechReviewPayload } from "../../lib/save-payload-utils";
|
||||
import type { TechReviewOpinionSectionHandle } from "../sections/tech-review-opinion-section";
|
||||
|
||||
interface FloatingActionButtonsProps {
|
||||
data: DesignCnstnMngDetResponseDto;
|
||||
docSeComCd: string;
|
||||
designCnstnDocNo: string;
|
||||
prgrsSttsComCd: string;
|
||||
dtlInstCd: string | undefined;
|
||||
atrzSeComCdList: string[];
|
||||
isRvwLoginUserLoading: boolean;
|
||||
techReviewOpinionRef?: React.RefObject<TechReviewOpinionSectionHandle | null>;
|
||||
opnrNm?: string;
|
||||
}
|
||||
|
||||
type ActiveDialog = "elaprTmplt" | "elaprApproval" | null;
|
||||
|
||||
function FloatingActionButtons({
|
||||
data,
|
||||
docSeComCd,
|
||||
designCnstnDocNo,
|
||||
prgrsSttsComCd,
|
||||
dtlInstCd,
|
||||
atrzSeComCdList,
|
||||
isRvwLoginUserLoading,
|
||||
techReviewOpinionRef,
|
||||
opnrNm,
|
||||
}: FloatingActionButtonsProps) {
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
const queryClient = useQueryClient();
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [activeDialog, setActiveDialog] = useState<ActiveDialog>(null);
|
||||
|
||||
const docNo = data.detDto?.designCnstnDocNo ?? designCnstnDocNo;
|
||||
const isRegister = data.detDto?.rgtr === userInfo?.userUnqId;
|
||||
|
||||
const needsAtrzResolve = needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd);
|
||||
const needsCsApprovalCheck = needsAtrzResolve && prgrsSttsComCd === "CS";
|
||||
const { data: elaprDocPrgrsList = [], isLoading: isElaprDocPrgrsLoading } =
|
||||
useElaprDocPrgrsByDocNo(needsCsApprovalCheck ? docNo : null);
|
||||
|
||||
const hasPendingApprovalRequest = hasCurrentUserPendingApprovalRequest(
|
||||
elaprDocPrgrsList,
|
||||
userInfo?.userUnqId,
|
||||
);
|
||||
|
||||
const actionButtons = useMemo(() => {
|
||||
if (needsAtrzResolve && isRvwLoginUserLoading) return [];
|
||||
if (needsCsApprovalCheck && isElaprDocPrgrsLoading) return [];
|
||||
return getButtonConfigs(dtlInstCd, prgrsSttsComCd, atrzSeComCdList, {
|
||||
hasPendingApprovalRequest: needsCsApprovalCheck ? hasPendingApprovalRequest : undefined,
|
||||
});
|
||||
}, [
|
||||
atrzSeComCdList,
|
||||
dtlInstCd,
|
||||
hasPendingApprovalRequest,
|
||||
isElaprDocPrgrsLoading,
|
||||
isRvwLoginUserLoading,
|
||||
needsAtrzResolve,
|
||||
needsCsApprovalCheck,
|
||||
prgrsSttsComCd,
|
||||
]);
|
||||
|
||||
const canEdit =
|
||||
isRegister && actionButtons.some((button) => button === DESIGN_CNSTN_VIEW_BUTTON_ACTION.strg);
|
||||
|
||||
function handleGoToList() {
|
||||
router.push("/design-cnstn");
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
router.push(`/design-cnstn/${designCnstnDocNo}/edit`);
|
||||
}
|
||||
|
||||
function shouldShowButton(action: DesignCnstnViewButtonAction) {
|
||||
return actionButtons.some((button) => button === action);
|
||||
}
|
||||
|
||||
async function executeAction({
|
||||
action,
|
||||
request,
|
||||
successMessage,
|
||||
}: {
|
||||
action: DesignCnstnViewButtonAction;
|
||||
request?: unknown;
|
||||
successMessage?: string;
|
||||
}) {
|
||||
const handler = buttonActionHandlers[action];
|
||||
if (!handler) return;
|
||||
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
await handler({
|
||||
request,
|
||||
data,
|
||||
onSuccess: () => {
|
||||
if (successMessage) alert(successMessage);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...designCnstnMngBaseKey, "findById", designCnstnDocNo],
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
let errorMessage = "";
|
||||
if (error instanceof AxiosError) {
|
||||
errorMessage = error.response?.data?.message ?? "";
|
||||
} else {
|
||||
errorMessage = error.message ?? "처리 중 오류가 발생했습니다.";
|
||||
}
|
||||
alert(errorMessage);
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStrgClick() {
|
||||
const payload: DesignCnstnApplySaveRequestDto = buildSavePayload(data, "CV");
|
||||
await executeAction({
|
||||
action: DESIGN_CNSTN_VIEW_BUTTON_ACTION.strg,
|
||||
request: payload,
|
||||
successMessage: "저장되었습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
async function handleAplyClick() {
|
||||
const payload: DesignCnstnApplySaveRequestDto = buildSavePayload(data, "CT");
|
||||
await executeAction({
|
||||
action: DESIGN_CNSTN_VIEW_BUTTON_ACTION.aply,
|
||||
request: payload,
|
||||
successMessage: "신청 완료되었습니다.",
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
}
|
||||
|
||||
async function handleDelClick() {
|
||||
if (!(await confirm("삭제하시겠습니까?"))) return;
|
||||
await executeAction({
|
||||
action: DESIGN_CNSTN_VIEW_BUTTON_ACTION.del,
|
||||
successMessage: "삭제되었습니다.",
|
||||
});
|
||||
router.push("/design-cnstn");
|
||||
}
|
||||
|
||||
function getReviewOpinionFormData() {
|
||||
return techReviewOpinionRef?.current?.getFormData() ?? null;
|
||||
}
|
||||
|
||||
async function handleElaprTmpltConfirm({ aplnRows }: ElaprTmpltPickerConfirmPayload) {
|
||||
const payload: ElaprDmndSaveRequestDto = {
|
||||
elaprDocNo: docNo,
|
||||
dmndTtl: data.detDto?.bizNm ?? "",
|
||||
dmndCn: data.detDto?.bizCn ?? "",
|
||||
elaprPrgrsSttsList: buildElaprPrgrsSttsListFromAplnRows(aplnRows),
|
||||
};
|
||||
await executeAction({
|
||||
action: DESIGN_CNSTN_VIEW_BUTTON_ACTION.rqstRvw,
|
||||
request: payload,
|
||||
successMessage: "검토자지정이 완료되었습니다.",
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND, "findDocPrgrsByDocNo", docNo],
|
||||
});
|
||||
}
|
||||
|
||||
async function handleRvwStrgClick() {
|
||||
const payload = buildTechReviewPayload(
|
||||
data,
|
||||
"CS",
|
||||
"SV",
|
||||
"KECO",
|
||||
getReviewOpinionFormData(),
|
||||
opnrNm,
|
||||
);
|
||||
|
||||
await executeAction({
|
||||
action: DESIGN_CNSTN_VIEW_BUTTON_ACTION.rvwStrg,
|
||||
request: payload,
|
||||
successMessage: "저장되었습니다.",
|
||||
});
|
||||
techReviewOpinionRef?.current?.resetForm();
|
||||
}
|
||||
|
||||
async function handleRvwCmptnClick() {
|
||||
const payload = buildTechReviewPayload(
|
||||
data,
|
||||
"CS",
|
||||
"CP",
|
||||
"KECO",
|
||||
getReviewOpinionFormData(),
|
||||
opnrNm,
|
||||
);
|
||||
|
||||
await executeAction({
|
||||
action: DESIGN_CNSTN_VIEW_BUTTON_ACTION.rvwCmptn,
|
||||
request: payload,
|
||||
});
|
||||
techReviewOpinionRef?.current?.resetForm();
|
||||
setActiveDialog("elaprApproval");
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="sticky bottom-0 z-40 !-mb-2 border-t bg-white pb-8 pt-6 shadow-[0_-4px_12px_rgba(0,0,0,0.06)]">
|
||||
<div className="mx-4 flex flex-wrap justify-center gap-3 sm:gap-4">
|
||||
<Button color="white" type="button" onClick={handleGoToList} disabled={isProcessing}>
|
||||
목록
|
||||
</Button>
|
||||
{canEdit && (
|
||||
<Button color="gray" onClick={handleEdit} disabled={isProcessing}>
|
||||
수정
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(DESIGN_CNSTN_VIEW_BUTTON_ACTION.strg) && (
|
||||
<Button onClick={handleStrgClick} disabled={isProcessing}>
|
||||
저장
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(DESIGN_CNSTN_VIEW_BUTTON_ACTION.aply) && (
|
||||
<Button color="green" onClick={handleAplyClick} disabled={isProcessing}>
|
||||
신청완료
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(DESIGN_CNSTN_VIEW_BUTTON_ACTION.del) && (
|
||||
<Button color="red" onClick={handleDelClick} disabled={isProcessing}>
|
||||
삭제
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(DESIGN_CNSTN_VIEW_BUTTON_ACTION.rqstRvw) && (
|
||||
<Button onClick={() => setActiveDialog("elaprTmplt")} disabled={isProcessing}>
|
||||
검토자지정
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(DESIGN_CNSTN_VIEW_BUTTON_ACTION.rvwStrg) && (
|
||||
<Button onClick={handleRvwStrgClick} disabled={isProcessing}>
|
||||
저장
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(DESIGN_CNSTN_VIEW_BUTTON_ACTION.rvwCmptn) && (
|
||||
<Button color="green" onClick={handleRvwCmptnClick} disabled={isProcessing}>
|
||||
검토완료
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(DESIGN_CNSTN_VIEW_BUTTON_ACTION.atrzAprv) && (
|
||||
<Button onClick={() => setActiveDialog("elaprApproval")} disabled={isProcessing}>
|
||||
결재(승인)
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(DESIGN_CNSTN_VIEW_BUTTON_ACTION.atrzRply) && (
|
||||
<Button onClick={() => setActiveDialog("elaprApproval")} disabled={isProcessing}>
|
||||
결재(회신)
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElaprTmpltPickerDialog
|
||||
open={activeDialog === "elaprTmplt"}
|
||||
onClose={() => setActiveDialog(null)}
|
||||
docSeComCd={docSeComCd}
|
||||
onConfirm={handleElaprTmpltConfirm}
|
||||
/>
|
||||
|
||||
<ElaprApprovalDialog
|
||||
open={activeDialog === "elaprApproval"}
|
||||
onOpenChange={(open) => setActiveDialog(open ? "elaprApproval" : null)}
|
||||
elaprDocNo={data.detDto?.designCnstnDocNo ?? designCnstnDocNo}
|
||||
currentUserUuid={userInfo?.userUnqId ?? ""}
|
||||
onSuccess={() => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...designCnstnMngBaseKey, "findById", designCnstnDocNo],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { FloatingActionButtons };
|
||||
|
||||
async function handleSaveApply(
|
||||
context: ActionHandlerContext<DesignCnstnApplySaveRequestDto>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await designCnstnMngService.saveApply(context.request);
|
||||
context.onSuccess?.();
|
||||
} catch (error) {
|
||||
context.onError?.(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDel(context: ActionHandlerContext): Promise<void> {
|
||||
try {
|
||||
await designCnstnMngService.delete(context.data.detDto?.designCnstnDocNo ?? "");
|
||||
context.onSuccess?.();
|
||||
} catch (error) {
|
||||
context.onError?.(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTechReview(
|
||||
context: ActionHandlerContext<DesignCnstnApplySaveRequestDto>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await designCnstnMngService.techReview(
|
||||
context.data.detDto?.designCnstnDocNo ?? "",
|
||||
context.request,
|
||||
);
|
||||
context.onSuccess?.();
|
||||
} catch (error) {
|
||||
context.onError?.(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRqstRvw(
|
||||
context: ActionHandlerContext<ElaprDmndSaveRequestDto>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await elaprDmndService.save(context.request);
|
||||
context.onSuccess?.();
|
||||
} catch (error) {
|
||||
context.onError?.(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAtrzAprv(context: ActionHandlerContext): Promise<void> {
|
||||
context.onSuccess?.();
|
||||
}
|
||||
|
||||
async function handleAtrzRply(context: ActionHandlerContext): Promise<void> {
|
||||
context.onSuccess?.();
|
||||
}
|
||||
|
||||
const buttonActionHandlers: Record<DesignCnstnViewButtonAction, ActionHandler> = {
|
||||
strg: handleSaveApply,
|
||||
aply: handleSaveApply,
|
||||
del: handleDel,
|
||||
rqstRvw: handleRqstRvw,
|
||||
rvwStrg: handleTechReview,
|
||||
rvwCmptn: handleTechReview,
|
||||
atrzAprv: handleAtrzAprv,
|
||||
atrzRply: handleAtrzRply,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
interface ActionHandlerContext<T = any> {
|
||||
request: T;
|
||||
data: DesignCnstnMngDetResponseDto;
|
||||
onSuccess?: () => void;
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
type ActionHandler = (context: ActionHandlerContext) => Promise<void>;
|
||||
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import { useDesignCnstnMng } from "@/entities/design-cnstn";
|
||||
import { Skeleton } from "@common/components";
|
||||
|
||||
import { DesignFormPage } from "./design-form-page";
|
||||
|
||||
interface DesignEditPageProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
function DesignEditPage({ id }: DesignEditPageProps) {
|
||||
const { data, isLoading } = useDesignCnstnMng(id);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="mt-6 space-y-6 px-4">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-[4rem] w-full" />
|
||||
<Skeleton className="h-[20rem] w-full" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-[4rem] w-full" />
|
||||
<Skeleton className="h-[20rem] w-full" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-[4rem] w-full" />
|
||||
<Skeleton className="h-[20rem] w-full" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data?.detDto) {
|
||||
return <div className="mt-6 text-[1.4rem]">상세 정보를 찾을 수 없습니다.</div>;
|
||||
}
|
||||
|
||||
return <DesignFormPage id={id} mode="edit" initialData={data} />;
|
||||
}
|
||||
|
||||
export { DesignEditPage };
|
||||
@@ -0,0 +1,209 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import {
|
||||
type DesignCnstnApplySaveRequestDto,
|
||||
designCnstnMngBaseKey,
|
||||
type DesignCnstnMngDetResponseDto,
|
||||
designCnstnMngService,
|
||||
} from "@/entities/design-cnstn";
|
||||
import {
|
||||
AttachmentsFormSection,
|
||||
BusinessScaleFormSection,
|
||||
ContactInfoFormSection,
|
||||
PrivacyConsentSection,
|
||||
} from "@/widgets/pbadms-sprt";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import {
|
||||
type DesignCnstnRegistFormData,
|
||||
designCnstnRegistSchema,
|
||||
formatTaskSeComCdForApi,
|
||||
} from "../lib/design-cnstn-regist-schema";
|
||||
import { getCreateDefaultValues, mapDetailToFormData } from "../lib/form-data-mapper";
|
||||
|
||||
import { BasicInfoFormSection } from "./sections/basic-info-form-section";
|
||||
|
||||
interface DesignFormPageProps {
|
||||
id?: string;
|
||||
mode: "create" | "edit";
|
||||
initialData?: DesignCnstnMngDetResponseDto;
|
||||
taskSeComCd?: string[];
|
||||
}
|
||||
|
||||
function DesignFormPage({ id, mode, initialData, taskSeComCd }: DesignFormPageProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const isEditMode = mode === "edit";
|
||||
|
||||
const form = useForm<DesignCnstnRegistFormData>({
|
||||
resolver: zodResolver(designCnstnRegistSchema),
|
||||
mode: "onChange",
|
||||
defaultValues:
|
||||
isEditMode && initialData
|
||||
? mapDetailToFormData(initialData)
|
||||
: getCreateDefaultValues(userInfo, taskSeComCd),
|
||||
});
|
||||
|
||||
function buildFormPayload(
|
||||
data: DesignCnstnRegistFormData,
|
||||
prgrsSttsComCd: "CV" | "CT",
|
||||
): DesignCnstnApplySaveRequestDto {
|
||||
const { taskSeComCd: formTaskSeComCd, ...designCnstnMngRest } = data.designCnstnMng;
|
||||
|
||||
return {
|
||||
designCnstnMng: {
|
||||
...designCnstnMngRest,
|
||||
taskSeComCd: formatTaskSeComCdForApi(formTaskSeComCd),
|
||||
designCnstnDocNo: isEditMode ? initialData?.detDto?.designCnstnDocNo : undefined,
|
||||
prgrsSttsComCd,
|
||||
},
|
||||
bzScaleList: data.bzScaleList,
|
||||
esntlAtchFileList: data.esntlAtchFileList,
|
||||
};
|
||||
}
|
||||
|
||||
async function submitForm(
|
||||
data: DesignCnstnRegistFormData,
|
||||
prgrsSttsComCd: "CV" | "CT",
|
||||
): Promise<string | undefined> {
|
||||
return designCnstnMngService.saveApply(buildFormPayload(data, prgrsSttsComCd));
|
||||
}
|
||||
|
||||
async function onSave(data: DesignCnstnRegistFormData) {
|
||||
if (isSubmitting) return;
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const designCnstnDocNo = await submitForm(data, "CV");
|
||||
if (!designCnstnDocNo) return;
|
||||
|
||||
alert("저장되었습니다.");
|
||||
router.push(`/design-cnstn/${designCnstnDocNo}/edit`);
|
||||
if (isEditMode && id) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...designCnstnMngBaseKey, "findById", id],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("저장 실패:", error);
|
||||
alert("저장 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onApply(data: DesignCnstnRegistFormData) {
|
||||
if (isSubmitting) return;
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const designCnstnDocNo = await submitForm(data, "CT");
|
||||
if (!designCnstnDocNo) return;
|
||||
|
||||
alert("신청 완료되었습니다.");
|
||||
router.push(`/design-cnstn/${designCnstnDocNo}`);
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
if (id) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...designCnstnMngBaseKey, "findById", id],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("신청 실패:", error);
|
||||
alert("신청 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleApplyClick() {
|
||||
form.handleSubmit(onApply)();
|
||||
}
|
||||
|
||||
async function handleDelClick() {
|
||||
if (isSubmitting) return;
|
||||
|
||||
const docNo = initialData?.detDto?.designCnstnDocNo;
|
||||
if (!docNo) {
|
||||
alert("문서번호가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await confirm("삭제하시겠습니까?"))) return;
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
await designCnstnMngService.delete(docNo);
|
||||
alert("삭제되었습니다.");
|
||||
router.push("/design-cnstn");
|
||||
} catch (error) {
|
||||
console.error("삭제 실패:", error);
|
||||
alert("삭제 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSave)} className="mt-6 space-y-4">
|
||||
<BasicInfoFormSection />
|
||||
<ContactInfoFormSection fieldPrefix="designCnstnMng" />
|
||||
<PrivacyConsentSection />
|
||||
<BusinessScaleFormSection />
|
||||
<AttachmentsFormSection
|
||||
docSeComCd="CNFM"
|
||||
fieldPrefix="designCnstnMng"
|
||||
initialEsntlFiles={isEditMode ? initialData?.esntlFileList : undefined}
|
||||
initialEtcFiles={isEditMode ? initialData?.etcFileList : undefined}
|
||||
/>
|
||||
|
||||
<div className="sticky bottom-0 z-40 !-mb-2 border-t bg-white pb-8 pt-6 shadow-[0_-4px_12px_rgba(0,0,0,0.06)]">
|
||||
<div className="mx-4 flex flex-wrap justify-center gap-3 sm:gap-4">
|
||||
<Button
|
||||
color="white"
|
||||
type="button"
|
||||
onClick={() => router.push("/design-cnstn")}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
목록
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
저장
|
||||
</Button>
|
||||
{isEditMode && (
|
||||
<>
|
||||
<Button
|
||||
color="green"
|
||||
type="button"
|
||||
onClick={handleApplyClick}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
신청완료
|
||||
</Button>
|
||||
<Button color="red" type="button" onClick={handleDelClick} disabled={isSubmitting}>
|
||||
삭제
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export { DesignFormPage };
|
||||
@@ -0,0 +1,152 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import type { PaginationState } from "@tanstack/react-table";
|
||||
|
||||
import {
|
||||
type CmCdAdstDstrComboResponseDto,
|
||||
useFindGugunCombo,
|
||||
useFindSidoCombo,
|
||||
} from "@/entities/adstdstr";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import type { DesignCnstnMngSearchDto } from "@/entities/design-cnstn";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
|
||||
import { DesignList } from "./design-list";
|
||||
|
||||
const DEFAULT_PAGE_INDEX = 0;
|
||||
const DEFAULT_PAGE_SIZE = 10;
|
||||
const DEFAULT_SORT = "designCnstnDocNo,desc";
|
||||
const EMPTY_ADST_LIST: CmCdAdstDstrComboResponseDto[] = [];
|
||||
|
||||
const DEFAULT_SEARCH_PARAMS: DesignCnstnMngSearchDto = {
|
||||
bizNm: "",
|
||||
taskSeComCd: "",
|
||||
admdstCd: "",
|
||||
page: DEFAULT_PAGE_INDEX,
|
||||
size: DEFAULT_PAGE_SIZE,
|
||||
sort: DEFAULT_SORT,
|
||||
};
|
||||
|
||||
const ALL_SELECT_VALUE = "__ALL__";
|
||||
|
||||
function DesignListPage() {
|
||||
const [bizNm, setBizNm] = useState("");
|
||||
const [taskSeComCd, setTaskSeComCd] = useState("");
|
||||
const [sidoComCd, setSidoComCd] = useState("");
|
||||
const [sggComCd, setSggComCd] = useState("");
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: DEFAULT_PAGE_INDEX,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
});
|
||||
const [searchParams, setSearchParams] = useState<DesignCnstnMngSearchDto>(DEFAULT_SEARCH_PARAMS);
|
||||
|
||||
const { data: sidoData } = useFindSidoCombo();
|
||||
const { data: gugunData } = useFindGugunCombo(sidoComCd);
|
||||
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
|
||||
const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
|
||||
|
||||
const handleSearch = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setPagination((prev) => ({ ...prev, pageIndex: DEFAULT_PAGE_INDEX }));
|
||||
setSearchParams({
|
||||
...DEFAULT_SEARCH_PARAMS,
|
||||
bizNm,
|
||||
taskSeComCd,
|
||||
admdstCd: sggComCd || sidoComCd || "",
|
||||
page: DEFAULT_PAGE_INDEX,
|
||||
size: pagination.pageSize,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 px-[1.8rem] pb-6">
|
||||
<section className="card card-border mx-0 bg-background">
|
||||
<form className="flex flex-wrap items-center gap-4 p-2" onSubmit={handleSearch}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="sidoComCd" className="whitespace-nowrap">
|
||||
시도
|
||||
</Label>
|
||||
<Select
|
||||
value={sidoComCd}
|
||||
onValueChange={(value) => {
|
||||
setSidoComCd(value === ALL_SELECT_VALUE ? "" : value);
|
||||
setSggComCd("");
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="sidoComCd" className="w-[12rem]">
|
||||
<SelectValue placeholder="전체" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={ALL_SELECT_VALUE}>전체</SelectItem>
|
||||
{sidoOptions.map((option) => (
|
||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||
{option.adstDstrNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="sggComCd" className="whitespace-nowrap">
|
||||
시군구
|
||||
</Label>
|
||||
<Select
|
||||
value={sggComCd}
|
||||
onValueChange={(value) => setSggComCd(value === ALL_SELECT_VALUE ? "" : value)}
|
||||
disabled={!sidoComCd}
|
||||
>
|
||||
<SelectTrigger id="sggComCd" className="w-[12rem]">
|
||||
<SelectValue placeholder="전체" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={ALL_SELECT_VALUE}>전체</SelectItem>
|
||||
{gugunOptions.map((option) => (
|
||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||
{option.adstDstrNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="taskSeComCd" className="whitespace-nowrap">
|
||||
구분
|
||||
</Label>
|
||||
<CodeSelect upCd="CNFM" value={taskSeComCd} onValueChange={setTaskSeComCd} />
|
||||
</div>
|
||||
<div className="flex w-[24rem] items-center gap-2">
|
||||
<Label htmlFor="bizNm" className="whitespace-nowrap">
|
||||
사업명
|
||||
</Label>
|
||||
<Input
|
||||
id="bizNm"
|
||||
value={bizNm}
|
||||
onChange={(event) => setBizNm(event.target.value)}
|
||||
placeholder="사업명"
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit">조회</Button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<DesignList
|
||||
searchParams={searchParams}
|
||||
pagination={pagination}
|
||||
onPaginationChange={setPagination}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { DesignListPage };
|
||||
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
|
||||
import { useDataTable, useUserStore } from "@/shared/lib";
|
||||
import {
|
||||
type DesignCnstnMngResponseDto,
|
||||
type DesignCnstnMngSearchDto,
|
||||
useDesignCnstnMngPage,
|
||||
} from "@/entities/design-cnstn";
|
||||
import { TaskSePickerDialog } from "@/widgets/pbadms-sprt";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import { formatTaskSeComCdForApi } from "../lib/design-cnstn-regist-schema";
|
||||
import { createDesignCnstnColumns } from "../model/columns";
|
||||
|
||||
interface DesignListProps {
|
||||
searchParams: DesignCnstnMngSearchDto;
|
||||
pagination: PaginationState;
|
||||
onPaginationChange: (pagination: PaginationState) => void;
|
||||
}
|
||||
|
||||
function DesignList({ searchParams, pagination, onPaginationChange }: DesignListProps) {
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const router = useRouter();
|
||||
const [isTaskSeDialogOpen, setIsTaskSeDialogOpen] = useState(false);
|
||||
|
||||
const queryParams = useMemo(
|
||||
() => ({
|
||||
bizNm: searchParams.bizNm,
|
||||
taskSeComCd: searchParams.taskSeComCd,
|
||||
admdstCd: searchParams.admdstCd,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
sort: searchParams.sort ?? "designCnstnDocNo,desc",
|
||||
}),
|
||||
[searchParams, pagination],
|
||||
);
|
||||
|
||||
const pageQuery = useDesignCnstnMngPage(queryParams);
|
||||
const columns = useMemo(() => createDesignCnstnColumns(), []);
|
||||
|
||||
const handleRowClick = (row: DesignCnstnMngResponseDto) => {
|
||||
const canEdit = row.prgrsSttsComCd === "CV" && row.rgtr === userInfo?.userUnqId;
|
||||
|
||||
router.push(
|
||||
canEdit
|
||||
? `/design-cnstn/${row.designCnstnDocNo}/edit`
|
||||
: `/design-cnstn/${row.designCnstnDocNo}`,
|
||||
);
|
||||
};
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
query: pageQuery,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: { pagination },
|
||||
onPaginationChange: (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
onPaginationChange(next);
|
||||
},
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
viewPagination: true,
|
||||
onRowClick: handleRowClick,
|
||||
},
|
||||
});
|
||||
|
||||
const handleConfirmTaskSe = (selectedCodes: string[]) => {
|
||||
setIsTaskSeDialogOpen(false);
|
||||
router.push(`/design-cnstn/regist?taskSeComCd=${formatTaskSeComCdForApi(selectedCodes)}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="card card-border mx-0 bg-background pb-8 pt-0">
|
||||
<div className="card-header flex items-center justify-between">
|
||||
<h3 className="text-2xl font-semibold">설계자문 목록</h3>
|
||||
<Button type="button" color="green" onClick={() => setIsTaskSeDialogOpen(true)}>
|
||||
신청
|
||||
</Button>
|
||||
</div>
|
||||
{tableComponent}
|
||||
</section>
|
||||
<TaskSePickerDialog
|
||||
open={isTaskSeDialogOpen}
|
||||
onOpenChange={setIsTaskSeDialogOpen}
|
||||
onConfirm={handleConfirmTaskSe}
|
||||
title="설계자문 신청"
|
||||
codeUpCd="CNFM"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { DesignList };
|
||||
@@ -0,0 +1,11 @@
|
||||
import { DesignFormPage } from "./design-form-page";
|
||||
|
||||
interface DesignRegistPageProps {
|
||||
taskSeComCd?: string[];
|
||||
}
|
||||
|
||||
function DesignRegistPage({ taskSeComCd }: DesignRegistPageProps) {
|
||||
return <DesignFormPage mode="create" taskSeComCd={taskSeComCd} />;
|
||||
}
|
||||
|
||||
export { DesignRegistPage };
|
||||
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useRef } from "react";
|
||||
|
||||
import { useUserStore } from "@/shared/lib";
|
||||
import { useDesignCnstnMng } from "@/entities/design-cnstn";
|
||||
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
|
||||
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
|
||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
import { Skeleton } from "@common/components";
|
||||
|
||||
import { needsRoleUser01AtrzResolve } from "../lib/button-config";
|
||||
import { getEditableReviewStep } from "../lib/review-step-utils";
|
||||
|
||||
import { FloatingActionButtons } from "./components/floating-action-buttons";
|
||||
import { AttachmentsSection } from "./sections/attachments-section";
|
||||
import { BasicInfoSection } from "./sections/basic-info-section";
|
||||
import { BusinessScaleSection } from "./sections/business-scale-section";
|
||||
import { ContactInfoSection } from "./sections/contact-info-section";
|
||||
import {
|
||||
TechReviewOpinionSection,
|
||||
type TechReviewOpinionSectionHandle,
|
||||
} from "./sections/tech-review-opinion-section";
|
||||
|
||||
interface DesignViewPageProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
function DesignViewPage({ id }: DesignViewPageProps) {
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const techReviewOpinionRef = useRef<TechReviewOpinionSectionHandle>(null);
|
||||
const { data, isLoading, isError } = useDesignCnstnMng(id);
|
||||
|
||||
const prgrsSttsComCd = data?.detDto?.prgrsSttsComCd || "";
|
||||
const needsAtrzResolve = needsRoleUser01AtrzResolve(userInfo?.dtlInstCd, prgrsSttsComCd);
|
||||
const { data: rvwLoginUserData, isLoading: isRvwLoginUserLoading } = useRvwMnpwMngLoginUserList(
|
||||
{},
|
||||
{ enabled: needsAtrzResolve },
|
||||
);
|
||||
const atrzSeComCdList = useMemo(
|
||||
() => rvwLoginUserData?.content?.map((item) => item.atrzSeComCd) ?? [],
|
||||
[rvwLoginUserData],
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="mt-6 flex flex-col gap-4 px-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-[4rem] rounded-xl" />
|
||||
<Skeleton className="h-[16rem] rounded-xl" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-[4rem] rounded-xl" />
|
||||
<Skeleton className="h-[16rem] rounded-xl" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-[4rem] rounded-xl" />
|
||||
<Skeleton className="h-[32rem] rounded-xl" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !data?.detDto) {
|
||||
return <div className="mt-6 text-[1.4rem]">상세 정보를 찾을 수 없습니다.</div>;
|
||||
}
|
||||
|
||||
const detDto = data.detDto;
|
||||
const bizSclList = data.bizSclList ?? [];
|
||||
const esntlFileList = data.esntlFileList ?? [];
|
||||
const etcFileList = data.etcFileList ?? [];
|
||||
const rvwList = data.rvwList ?? [];
|
||||
|
||||
const editableReviewStep = getEditableReviewStep(
|
||||
userInfo?.dtlInstCd,
|
||||
detDto.prgrsSttsComCd || "",
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mt-6 space-y-4">
|
||||
<BasicInfoSection det={detDto} />
|
||||
<ContactInfoSection det={detDto} />
|
||||
<SectionCollapsible title="결재현황">
|
||||
<ElaprDocPrgrsStatus elaprDocNo={detDto.designCnstnDocNo} />
|
||||
</SectionCollapsible>
|
||||
<BusinessScaleSection bizSclList={bizSclList} />
|
||||
<AttachmentsSection esntlFileList={esntlFileList} etcFileList={etcFileList} />
|
||||
<TechReviewOpinionSection
|
||||
ref={techReviewOpinionRef}
|
||||
rvwList={rvwList}
|
||||
editableReviewStep={editableReviewStep}
|
||||
atrzSeComCdList={atrzSeComCdList}
|
||||
isAtrzLoading={needsAtrzResolve && isRvwLoginUserLoading}
|
||||
/>
|
||||
<FloatingActionButtons
|
||||
data={data}
|
||||
docSeComCd="CNFM"
|
||||
designCnstnDocNo={detDto.designCnstnDocNo}
|
||||
prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
|
||||
dtlInstCd={userInfo?.dtlInstCd}
|
||||
atrzSeComCdList={atrzSeComCdList}
|
||||
isRvwLoginUserLoading={needsAtrzResolve && isRvwLoginUserLoading}
|
||||
techReviewOpinionRef={techReviewOpinionRef}
|
||||
opnrNm={userInfo?.userName}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { DesignViewPage };
|
||||
@@ -0,0 +1,112 @@
|
||||
"use client";
|
||||
|
||||
import { formatFileSize } from "@/shared/lib/file-utils";
|
||||
import { useDownloadAttachmentByAtflCd } from "@/entities/attach";
|
||||
import type {
|
||||
AtflMngClientResponseDto,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
} from "@/entities/design-cnstn";
|
||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
|
||||
interface AttachmentsSectionProps {
|
||||
esntlFileList: EsntlAtchFileMngTreeResponseDto[];
|
||||
etcFileList: AtflMngClientResponseDto[];
|
||||
}
|
||||
|
||||
function AttachmentsSection({ esntlFileList, etcFileList }: AttachmentsSectionProps) {
|
||||
const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd();
|
||||
|
||||
return (
|
||||
<>
|
||||
{esntlFileList.length > 0 && (
|
||||
<SectionCollapsible title="필수 첨부파일">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>필수 자료명</TableHead>
|
||||
<TableHead>파일명</TableHead>
|
||||
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{esntlFileList.map((file) => (
|
||||
<TableRow key={file.esntlAtchFileId}>
|
||||
<TableCell className="pl-2 text-left">
|
||||
{file.esntlAtchFileNm}
|
||||
{file.esntlAtchYn === "Y" ? (
|
||||
<span className="ml-1 text-red-500">*</span>
|
||||
) : null}
|
||||
</TableCell>
|
||||
<TableCell className="text-left">
|
||||
{file.atflCd ? (
|
||||
<a
|
||||
className="cursor-pointer text-blue-600 hover:underline"
|
||||
onClick={() => downloadAttachment(file.atflCd ?? "")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{file.orgnlFileNm || "다운로드"}
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-gray-500">첨부없음</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">{formatFileSize(file.atflFlieSz)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
)}
|
||||
|
||||
<SectionCollapsible title="기타 첨부파일">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>파일명</TableHead>
|
||||
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{etcFileList.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} className="py-12 text-center text-muted-foreground">
|
||||
등록된 기타첨부파일이 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
etcFileList.map((file) => (
|
||||
<TableRow key={file.atflId}>
|
||||
<TableCell className="text-left">
|
||||
{file.atflCd ? (
|
||||
<a
|
||||
className="cursor-pointer text-blue-600 hover:underline"
|
||||
onClick={() => downloadAttachment(file.atflCd)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{file.orgnlFileNm || file.physFileNm || file.atflId}
|
||||
</a>
|
||||
) : (
|
||||
<span>{file.orgnlFileNm || file.physFileNm || file.atflId}</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-gray-500">
|
||||
{formatFileSize(file.atflFlieSz)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { AttachmentsSection };
|
||||
@@ -0,0 +1,159 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { useUserStore } from "@/shared/lib";
|
||||
import { Field, FieldError, FieldGroup } from "@/shared/ui/field";
|
||||
import { findCodeLabel, useCodeList } from "@/entities/code";
|
||||
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
|
||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Table,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
import type { DesignCnstnRegistFormData } from "../../lib/design-cnstn-regist-schema";
|
||||
|
||||
function BasicInfoFormSection() {
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const { control, watch, setValue } = useFormContext<DesignCnstnRegistFormData>();
|
||||
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
||||
const taskSeComCd = watch("designCnstnMng.taskSeComCd");
|
||||
const { data: taskOptions } = useCodeList("CNFM");
|
||||
|
||||
function handleBnsSelect(data: BnsApplyData) {
|
||||
setValue("designCnstnMng.bizNm", data.bizNm);
|
||||
setValue("designCnstnMng.bizBgngYr", data.bizBgngYr);
|
||||
setValue("designCnstnMng.bizEndYr", data.bizEndYr);
|
||||
setValue("designCnstnMng.bizCn", data.bizCn);
|
||||
}
|
||||
|
||||
const taskSeComCdLabel =
|
||||
taskSeComCd
|
||||
.map((code) => findCodeLabel(taskOptions, code))
|
||||
.filter(Boolean)
|
||||
.join(", ") || "-";
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]" data-required>
|
||||
사업명
|
||||
</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<Controller
|
||||
name="designCnstnMng.bizNm"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
{...field}
|
||||
id={field.name}
|
||||
placeholder="사업명 입력"
|
||||
aria-invalid={fieldState.invalid}
|
||||
className="w-[40rem]"
|
||||
/>
|
||||
<Button type="button" onClick={() => setIsPickerOpen(true)}>
|
||||
사업찾기
|
||||
</Button>
|
||||
</div>
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>사업기간</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<FieldGroup className="flex-row items-center gap-2">
|
||||
<Controller
|
||||
name="designCnstnMng.bizBgngYr"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid} className="w-auto">
|
||||
<Input
|
||||
{...field}
|
||||
id={field.name}
|
||||
placeholder="시작연도"
|
||||
className="max-w-[12rem]"
|
||||
aria-invalid={fieldState.invalid}
|
||||
/>
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
<span>~</span>
|
||||
<Controller
|
||||
name="designCnstnMng.bizEndYr"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid} className="w-auto">
|
||||
<Input
|
||||
{...field}
|
||||
id={field.name}
|
||||
placeholder="종료연도"
|
||||
className="max-w-[12rem]"
|
||||
aria-invalid={fieldState.invalid}
|
||||
/>
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</FieldGroup>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>사업내용</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<Controller
|
||||
name="designCnstnMng.bizCn"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<Input
|
||||
{...field}
|
||||
id={field.name}
|
||||
placeholder="사업내용 입력"
|
||||
aria-invalid={fieldState.invalid}
|
||||
/>
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>구분</TableHead>
|
||||
<TableCell>{taskSeComCdLabel}</TableCell>
|
||||
<TableHead>신청 지자체</TableHead>
|
||||
<TableCell>{userInfo?.instSeNm}</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
</Table>
|
||||
|
||||
<BnsPickerDialog
|
||||
open={isPickerOpen}
|
||||
onClose={() => setIsPickerOpen(false)}
|
||||
onSelect={handleBnsSelect}
|
||||
/>
|
||||
</SectionCollapsible>
|
||||
);
|
||||
}
|
||||
|
||||
export { BasicInfoFormSection };
|
||||
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import type { DesignCnstnMngResponseDto } from "@/entities/design-cnstn";
|
||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
|
||||
interface BasicInfoSectionProps {
|
||||
det: DesignCnstnMngResponseDto;
|
||||
}
|
||||
|
||||
function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>사업명</TableHead>
|
||||
<TableCell>{det.bizNm || "-"}</TableCell>
|
||||
<TableHead>사업기간</TableHead>
|
||||
<TableCell>{(det.bizBgngYr || "-") + " ~ " + (det.bizEndYr || "-")}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사업내용</TableHead>
|
||||
<TableCell colSpan={3}>{det.bizCn || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>환경청구분</TableHead>
|
||||
<TableCell>{det.envoSeCdNm || "-"}</TableCell>
|
||||
<TableHead>구분</TableHead>
|
||||
<TableCell>{det.taskSeCdNm || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>신청지자체</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
{det.sido || "-"} {det.gugun || "-"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
</Table>
|
||||
</SectionCollapsible>
|
||||
);
|
||||
}
|
||||
|
||||
export { BasicInfoSection };
|
||||
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import type { BizSclTreeDto } from "@/entities/design-cnstn";
|
||||
import { fmtNumber, indentPx, SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
|
||||
interface BusinessScaleSectionProps {
|
||||
bizSclList: BizSclTreeDto[];
|
||||
}
|
||||
|
||||
function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="사업규모">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[30rem]">구분</TableHead>
|
||||
<TableHead>기본계획(A)</TableHead>
|
||||
<TableHead>예산반영(B)</TableHead>
|
||||
<TableHead>금회설계시(C)</TableHead>
|
||||
<TableHead>증감(C-B)</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{bizSclList.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="py-12 text-center text-muted-foreground">
|
||||
조회된 사업규모 항목이 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
bizSclList.map((node) => {
|
||||
const pl = indentPx(node.indCnstpSeCdNm);
|
||||
const bscPlanQty = Number(node.bscPlanQty) || 0;
|
||||
const budgetReflct = Number(node.bgtRfltQty) || 0;
|
||||
const tstmDesignQty = Number(node.tstmDesignQty) || 0;
|
||||
const diff = tstmDesignQty - budgetReflct;
|
||||
|
||||
return (
|
||||
<TableRow key={node.cnstpSeCd}>
|
||||
<TableHead className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||
{node.cnstpSeCdNm}
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<div className="text-right">{fmtNumber(bscPlanQty)}</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-right">{fmtNumber(budgetReflct)}</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-right">{fmtNumber(tstmDesignQty)}</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-right">
|
||||
{diff > 0 ? `+${fmtNumber(diff)}` : fmtNumber(diff)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
);
|
||||
}
|
||||
|
||||
export { BusinessScaleSection };
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import type { DesignCnstnMngResponseDto } from "@/entities/design-cnstn";
|
||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
|
||||
interface ContactInfoSectionProps {
|
||||
det: DesignCnstnMngResponseDto;
|
||||
}
|
||||
|
||||
function ContactInfoSection({ det }: ContactInfoSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="담당자 정보">
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
{det.aplcntNm && (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2}>신청자</TableHead>
|
||||
<TableHead>이름</TableHead>
|
||||
<TableCell>{det.aplcntNm || "-"}</TableCell>
|
||||
<TableHead>연락처</TableHead>
|
||||
<TableCell>{det.aplcntMblTelno || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>소속</TableHead>
|
||||
<TableCell>{det.aplcntOgdpNm || "-"}</TableCell>
|
||||
<TableHead>이메일</TableHead>
|
||||
<TableCell>{det.aplcntEmlAddr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
)}
|
||||
{det.dsgnrNm && (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2}>설계자</TableHead>
|
||||
<TableHead>이름</TableHead>
|
||||
<TableCell>{det.dsgnrNm || "-"}</TableCell>
|
||||
<TableHead>연락처</TableHead>
|
||||
<TableCell>{det.dsgnrTelno || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>소속</TableHead>
|
||||
<TableCell>{det.dsgnrOgdpNm || "-"}</TableCell>
|
||||
<TableHead>이메일</TableHead>
|
||||
<TableCell>{det.dsgnrEmlAddr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
)}
|
||||
</TableHeader>
|
||||
</Table>
|
||||
</SectionCollapsible>
|
||||
);
|
||||
}
|
||||
|
||||
export { ContactInfoSection };
|
||||
@@ -0,0 +1,342 @@
|
||||
"use client";
|
||||
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||
|
||||
import { PROXY_HOST } from "@/shared/config";
|
||||
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||
import { attachService } from "@/entities/attach";
|
||||
import type { DesignCnstnRvwOpnnResponseDto } from "@/entities/design-cnstn";
|
||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
import {
|
||||
Button,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
import type {
|
||||
ReviewOpinionFormData,
|
||||
ReviewOpinionGetFormDataResult,
|
||||
} from "../../lib/review-opinion-utils";
|
||||
import type { ReviewStepCode } from "../../model/view-types";
|
||||
import { REVIEW_STEP_LABELS } from "../../model/view-types";
|
||||
|
||||
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
||||
|
||||
interface TechReviewOpinionSectionProps {
|
||||
rvwList: DesignCnstnRvwOpnnResponseDto[];
|
||||
editableReviewStep?: ReviewStepCode;
|
||||
atrzSeComCdList?: string[];
|
||||
isAtrzLoading?: boolean;
|
||||
}
|
||||
|
||||
export interface TechReviewOpinionSectionHandle {
|
||||
getFormData: () => ReviewOpinionGetFormDataResult | null;
|
||||
resetForm: () => void;
|
||||
}
|
||||
|
||||
interface TechReviewOpinionTableProps {
|
||||
items: DesignCnstnRvwOpnnResponseDto[];
|
||||
}
|
||||
|
||||
interface UploadedOpinionFile {
|
||||
fileName: string;
|
||||
fileKey: string;
|
||||
atflId?: string;
|
||||
atflCd?: string;
|
||||
}
|
||||
|
||||
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
|
||||
rvwRsltCn: "",
|
||||
rvwRsltAnmlyYn: "",
|
||||
atchFileGroupId: "",
|
||||
opnrSeComCd: "",
|
||||
};
|
||||
|
||||
function createRvwOpnnUploadKey(file: File) {
|
||||
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
||||
}
|
||||
|
||||
function hasFormContent(formData: ReviewOpinionFormData) {
|
||||
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
||||
}
|
||||
|
||||
const TechReviewOpinionSection = forwardRef<
|
||||
TechReviewOpinionSectionHandle,
|
||||
TechReviewOpinionSectionProps
|
||||
>(function TechReviewOpinionSection({
|
||||
rvwList,
|
||||
editableReviewStep,
|
||||
atrzSeComCdList = [],
|
||||
isAtrzLoading = false,
|
||||
}, ref) {
|
||||
const canWriteOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||
...EMPTY_FORM_DATA,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
});
|
||||
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
}));
|
||||
}, [editableReviewStep]);
|
||||
|
||||
const { uploadFileAsync, isUploading, uploads } = useTus({
|
||||
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
||||
if (!atflCd || !atflId) return;
|
||||
|
||||
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
|
||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
||||
},
|
||||
onError: (_error, _file, fileKey) => {
|
||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
||||
},
|
||||
});
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
getFormData: () => {
|
||||
if (!canWriteOpinion || !editableReviewStep || !hasFormContent(formData)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
formData: {
|
||||
...formData,
|
||||
opnrSeComCd: editableReviewStep,
|
||||
},
|
||||
rvwList,
|
||||
};
|
||||
},
|
||||
resetForm: () => {
|
||||
setFormData({
|
||||
...EMPTY_FORM_DATA,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
});
|
||||
setUploadedFile(null);
|
||||
},
|
||||
}),
|
||||
[canWriteOpinion, editableReviewStep, formData, rvwList],
|
||||
);
|
||||
|
||||
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
||||
const hasKecoSection =
|
||||
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteOpinion);
|
||||
|
||||
if (!hasKecoSection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function handleFileUpload(files: FileList | null) {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const file = files[0];
|
||||
const fileKey = createRvwOpnnUploadKey(file);
|
||||
const existingAtflCd = formData.atchFileGroupId;
|
||||
|
||||
setUploadedFile({ fileName: file.name, fileKey });
|
||||
|
||||
try {
|
||||
await uploadFileAsync(file, {
|
||||
fileKey,
|
||||
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
||||
});
|
||||
} catch {
|
||||
// onError callback handles cleanup
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFileRemove() {
|
||||
if (!uploadedFile) return;
|
||||
|
||||
uploads[uploadedFile.fileKey]?.abort();
|
||||
|
||||
if (uploadedFile.atflId) {
|
||||
try {
|
||||
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
||||
} catch (error) {
|
||||
console.error("검토의견 첨부파일 삭제 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
setUploadedFile(null);
|
||||
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
||||
}
|
||||
|
||||
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
||||
if (editableReviewStep !== reviewStep || !canWriteOpinion) return null;
|
||||
|
||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[12rem]">의견내용</TableHead>
|
||||
<TableCell>
|
||||
<Textarea
|
||||
value={formData.rvwRsltCn}
|
||||
onChange={(event) =>
|
||||
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
|
||||
}
|
||||
placeholder="의견내용을 입력하세요"
|
||||
rows={4}
|
||||
className="min-h-[8rem] !text-xl"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>검토결과</TableHead>
|
||||
<TableCell>
|
||||
<FieldSet>
|
||||
<RadioGroup
|
||||
value={formData.rvwRsltAnmlyYn}
|
||||
onValueChange={(value) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
|
||||
}))
|
||||
}
|
||||
>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<FieldLabel
|
||||
htmlFor={`rvw-opnn-normal-${reviewStep}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
|
||||
<span className="font-normal">정상</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel
|
||||
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
|
||||
<span className="font-normal">이상</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>첨부파일</TableHead>
|
||||
<TableCell>
|
||||
{uploadedFile ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl">{uploadedFile.fileName}</span>
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
void handleFileRemove();
|
||||
}}
|
||||
disabled={uploading}
|
||||
className="size-8 !bg-destructive"
|
||||
>
|
||||
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<label>
|
||||
<input
|
||||
type="file"
|
||||
className="hidden"
|
||||
onChange={(event) => {
|
||||
void handleFileUpload(event.target.files);
|
||||
event.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<Button size="sm" variant="outline" type="button" asChild>
|
||||
<span>파일 선택</span>
|
||||
</Button>
|
||||
</label>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
||||
<div className="space-y-6">
|
||||
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
|
||||
{renderOpinionForm("KECO")}
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
);
|
||||
});
|
||||
|
||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="55%" />
|
||||
<col width="5%" />
|
||||
<col width="20%" />
|
||||
<col width="10%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>의견자명</TableHead>
|
||||
<TableHead>의견내용</TableHead>
|
||||
<TableHead>검토결과</TableHead>
|
||||
<TableHead>첨부파일</TableHead>
|
||||
<TableHead>등록일시</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.map((item) => (
|
||||
<TableRow key={`${item.designCnstnDocNo}_${item.rvwOpnnSn}`}>
|
||||
<TableCell align="center">{item.opnrNm}</TableCell>
|
||||
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
|
||||
<TableCell align="center">
|
||||
{item.rvwRsltAnmlyYn === "Y" ? "이상" : item.rvwRsltAnmlyYn === "N" ? "정상" : "-"}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{item.atchFileInfo?.atflCd ? (
|
||||
<a
|
||||
className="text-blue-600 hover:underline"
|
||||
href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${item.atchFileInfo.atflCd}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{item.atchFileInfo.orgnlFileNm || "다운로드"}
|
||||
</a>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{item.regDt ? dayjs(item.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { TechReviewOpinionSection };
|
||||
@@ -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<string, Record<string, FnrsCnsltnViewBu
|
||||
FNRS_CNSLTN_VIEW_BUTTON_ACTION.aply,
|
||||
FNRS_CNSLTN_VIEW_BUTTON_ACTION.del,
|
||||
],
|
||||
SL: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.splmntCmptn],
|
||||
TA: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.fnrsCnsltnAncmnt],
|
||||
},
|
||||
ROLE_USER04: {
|
||||
@@ -44,10 +41,6 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, FnrsCnsltnViewBu
|
||||
LE: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply, FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw],
|
||||
TP: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply],
|
||||
},
|
||||
ROLE_USER01: {
|
||||
TE: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.grndsChck],
|
||||
SL: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.splmntCmptn],
|
||||
},
|
||||
};
|
||||
|
||||
const ROLE_USER01_ATRZ_STATUS_CODES = ["ER", "TE"] as const;
|
||||
|
||||
@@ -83,6 +83,24 @@ export const fnrsCnsltnRegistSchema = z.object({
|
||||
atchFileGroupId: z.string(),
|
||||
}),
|
||||
),
|
||||
splmntDmndList: z.array(
|
||||
z.object({
|
||||
splmntDmndSn: z.number().optional(),
|
||||
splmntArtclComCd: z.string().optional(),
|
||||
splmntArtclCdNm: z.string().nullish(),
|
||||
splmntDmndCn: z.string().optional(),
|
||||
splmntDmndDt: z.string().optional(),
|
||||
splmntDmndAtchFileGroupId: z.string().optional(),
|
||||
splmntDmndAtchFileNm: z.string().optional(),
|
||||
splmntCn: z.string().optional(),
|
||||
splmntPicNm: z.string().optional(),
|
||||
splmntCmptnDt: z.string().optional(),
|
||||
splmntAtchFileGroupId: z.string().optional(),
|
||||
splmntAtchFileNm: z.string().optional(),
|
||||
splmntAtchFileAtflId: z.string().optional(),
|
||||
cmptnYn: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
privacyConsent: z.enum(["Y", "N"], {
|
||||
required_error: "개인정보 수집/이용 동의는 필수입니다",
|
||||
}),
|
||||
|
||||
@@ -34,6 +34,7 @@ export function mapDetailToFormData(data: FnrsCnsltnMngDetResponseDto): FnrsCnsl
|
||||
const bizCstList = data.bizCstList ?? [];
|
||||
const cklstResultList = data.cklstResultList;
|
||||
const esntlFileList = data.esntlFileList;
|
||||
const splmntDmndList = data.splmntDmndList;
|
||||
|
||||
return {
|
||||
fnrsCnsltnMng: {
|
||||
@@ -94,6 +95,23 @@ export function mapDetailToFormData(data: FnrsCnsltnMngDetResponseDto): FnrsCnsl
|
||||
picRvwCn: item.picRvwCn,
|
||||
})) ?? [],
|
||||
esntlAtchFileList: mapEsntlAtchFileList(esntlFileList),
|
||||
splmntDmndList:
|
||||
splmntDmndList?.map((item) => ({
|
||||
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",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<string, string>();
|
||||
|
||||
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<string, string>();
|
||||
|
||||
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: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
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,
|
||||
Table,
|
||||
TableBody,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
import type { ReviewerOption } from "../../model/reviewer-option";
|
||||
|
||||
import { AprvrPickerDialog } from "./aprvr-picker-dialog";
|
||||
import { SortableAplnPickerRow } from "./sortable-apln-picker-row";
|
||||
|
||||
interface ElaprAplnPickerGridProps {
|
||||
elaprTmpltId: string | null;
|
||||
onRowsChange?: (rows: ElaprAplnRow[]) => void;
|
||||
onEditingLockedChange?: (locked: boolean) => void;
|
||||
}
|
||||
|
||||
function ElaprAplnPickerGrid({
|
||||
elaprTmpltId,
|
||||
onRowsChange,
|
||||
onEditingLockedChange,
|
||||
}: ElaprAplnPickerGridProps) {
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
const [pickingRowKey, setPickingRowKey] = useState<string | null>(null);
|
||||
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const aplnQuery = useElaprAplnPage({
|
||||
elaprTmpltId: elaprTmpltId ?? undefined,
|
||||
page: 0,
|
||||
size: 200,
|
||||
sort: "atrzSeqNo,asc",
|
||||
});
|
||||
|
||||
const {
|
||||
rows,
|
||||
visibleRows,
|
||||
editingRowKey,
|
||||
isEditingLocked,
|
||||
addRow,
|
||||
startEdit,
|
||||
updateRow,
|
||||
cancelEdit,
|
||||
commitEdit,
|
||||
markDeleted,
|
||||
reorderRows,
|
||||
} = useAplnRows({
|
||||
elaprTmpltId,
|
||||
serverRows: aplnQuery.data?.content,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onRowsChange?.(rows);
|
||||
}, [rows, onRowsChange]);
|
||||
|
||||
useEffect(() => {
|
||||
onEditingLockedChange?.(isEditingLocked);
|
||||
}, [isEditingLocked, onEditingLockedChange]);
|
||||
|
||||
const sensors = useDndSensors();
|
||||
const isDisabled = !elaprTmpltId;
|
||||
const pickingRow = pickingRowKey ? rows.find((r) => r.rowKey === pickingRowKey) : undefined;
|
||||
|
||||
function handleDragEnd(event: DragEndEvent) {
|
||||
const { active, over } = event;
|
||||
if (!over || active.id === over.id) return;
|
||||
|
||||
reorderRows(String(active.id), String(over.id));
|
||||
}
|
||||
|
||||
function handleRowChange(row: ElaprAplnRow, patch: Partial<ElaprAplnRow>) {
|
||||
updateRow(row.rowKey, patch);
|
||||
}
|
||||
|
||||
function handleRowSave(row: ElaprAplnRow) {
|
||||
const fieldError = validateAplnRow(row);
|
||||
if (fieldError) {
|
||||
alert(fieldError);
|
||||
return;
|
||||
}
|
||||
|
||||
commitEdit(row.rowKey);
|
||||
}
|
||||
|
||||
function handleRowDelete(row: ElaprAplnRow) {
|
||||
markDeleted(row.rowKey);
|
||||
}
|
||||
|
||||
function handleReviewerSelect(reviewer: ReviewerOption) {
|
||||
if (!pickingRowKey) return;
|
||||
updateRow(pickingRowKey, {
|
||||
aprvrUuid: reviewer.uuid,
|
||||
aprvrNm: reviewer.name,
|
||||
});
|
||||
setPickingRowKey(null);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="card card-border mx-0 min-h-[32rem] flex-1 pt-0">
|
||||
<div className="card-header flex flex-wrap items-center justify-between gap-3">
|
||||
<h3 className="text-2xl font-semibold">결재선 관리</h3>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button
|
||||
size="sm"
|
||||
type="button"
|
||||
color="green"
|
||||
disabled={isDisabled || isEditingLocked}
|
||||
onClick={addRow}
|
||||
>
|
||||
결재선 추가
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{aplnQuery.isPending && elaprTmpltId ? (
|
||||
<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">결재서식을 선택하세요.</p>
|
||||
) : visibleRows.length === 0 ? (
|
||||
<p className="py-8 text-center text-lg text-muted-foreground">
|
||||
등록된 결재선이 없습니다. [결재선 추가]로 결재자를 등록하세요.
|
||||
</p>
|
||||
) : (
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<Table className="w-full">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="30%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="30%" />
|
||||
<col width="10%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-10" />
|
||||
<TableHead>결재선아이디</TableHead>
|
||||
<TableHead>결재순서</TableHead>
|
||||
<TableHead>결재구분</TableHead>
|
||||
<TableHead>결재자</TableHead>
|
||||
<TableHead>관리</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<SortableContext
|
||||
items={visibleRows.map((r) => r.rowKey)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{visibleRows.map((row, index) => (
|
||||
<SortableAplnPickerRow
|
||||
key={row.rowKey}
|
||||
row={row}
|
||||
displaySeqNo={index + 1}
|
||||
isEditing={editingRowKey === row.rowKey}
|
||||
isLocked={isEditingLocked}
|
||||
dragDisabled={isEditingLocked || !row.atrzSeComCd}
|
||||
onStartEdit={() => startEdit(row.rowKey)}
|
||||
onSave={() => handleRowSave(row)}
|
||||
onCancel={cancelEdit}
|
||||
onDelete={() => handleRowDelete(row)}
|
||||
onChange={(patch) => handleRowChange(row, patch)}
|
||||
onPickAprvr={() => {
|
||||
setPickingRowKey(row.rowKey);
|
||||
setPickerOpen(true);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</DndContext>
|
||||
)}
|
||||
|
||||
<AprvrPickerDialog
|
||||
open={pickerOpen}
|
||||
onOpenChange={setPickerOpen}
|
||||
atrzSeComCd={pickingRow?.atrzSeComCd ?? ""}
|
||||
onSelect={handleReviewerSelect}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export { ElaprAplnPickerGrid };
|
||||
@@ -1,419 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { closestCenter, DndContext, type DragEndEvent } from "@dnd-kit/core";
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
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,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
import type { ReviewerOption } from "../../model/reviewer-option";
|
||||
|
||||
import { AprvrPickerDialog } from "./aprvr-picker-dialog";
|
||||
import { SortablePrgrsSttsRow } from "./sortable-prgrs-stts-row";
|
||||
|
||||
interface ElaprApprovalDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
elaprDocNo: string;
|
||||
currentUserUuid: string;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const UNSAVED_PRGRS_STTS_MESSAGE = "결재선 변경 이력이 있습니다.\n결재선 저장을 해 주세요.";
|
||||
|
||||
function ElaprApprovalDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
elaprDocNo,
|
||||
currentUserUuid,
|
||||
onSuccess,
|
||||
}: ElaprApprovalDialogProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const [rjctRsn, setRjctRsn] = useState("");
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
const [pickingRowKey, setPickingRowKey] = useState<string | null>(null);
|
||||
const [finalApprovalYn, setFinalApprovalYn] = useState(false);
|
||||
|
||||
const { data: elaprDocPrgrsList, isLoading } = useElaprDocPrgrsByDocNo(open ? elaprDocNo : null);
|
||||
|
||||
const elaprDmndId = elaprDocPrgrsList?.[0]?.elaprDmndId;
|
||||
|
||||
const {
|
||||
rows,
|
||||
visibleRows,
|
||||
editingRowKey,
|
||||
isEditingLocked,
|
||||
addRow,
|
||||
startEdit,
|
||||
updateRow,
|
||||
cancelEdit,
|
||||
commitEdit,
|
||||
markDeleted,
|
||||
reorderRows,
|
||||
} = usePrgrsSttsRows({
|
||||
serverRows: elaprDocPrgrsList,
|
||||
elaprDmndId,
|
||||
});
|
||||
|
||||
const savePrgrsSttsMutation = useSaveElaprPrgrsStts();
|
||||
const sensors = useDndSensors();
|
||||
|
||||
const currentUserPrgrs = findCurrentUserPendingApprovalRequest(
|
||||
elaprDocPrgrsList,
|
||||
currentUserUuid,
|
||||
);
|
||||
const canApprove = Boolean(currentUserPrgrs?.elaprPrgrsId);
|
||||
|
||||
const pickingRow = pickingRowKey ? rows.find((r) => r.rowKey === pickingRowKey) : undefined;
|
||||
const isBusy = isProcessing || savePrgrsSttsMutation.isPending;
|
||||
const isPrgrsSttsUnsaved = hasUnsavedPrgrsSttsChanges(rows, elaprDocPrgrsList ?? []);
|
||||
|
||||
function handleDragEnd(event: DragEndEvent) {
|
||||
const { active, over } = event;
|
||||
if (!over || active.id === over.id) return;
|
||||
|
||||
reorderRows(String(active.id), String(over.id));
|
||||
}
|
||||
|
||||
function handleRowSave(rowKey: string) {
|
||||
const row = rows.find((r) => r.rowKey === rowKey);
|
||||
if (!row) return;
|
||||
|
||||
const fieldError = validatePrgrsSttsRow(row);
|
||||
if (fieldError) {
|
||||
alert(fieldError);
|
||||
return;
|
||||
}
|
||||
|
||||
commitEdit(rowKey);
|
||||
}
|
||||
|
||||
function handleReviewerSelect(reviewer: ReviewerOption) {
|
||||
if (!pickingRowKey) return;
|
||||
updateRow(pickingRowKey, {
|
||||
aprvrUuid: reviewer.uuid,
|
||||
aprvrNm: reviewer.name,
|
||||
ognzNm: reviewer.deptNm,
|
||||
});
|
||||
setPickingRowKey(null);
|
||||
}
|
||||
|
||||
function assertPrgrsSttsSaved(): boolean {
|
||||
if (isEditingLocked) {
|
||||
alert(UNSAVED_PRGRS_STTS_MESSAGE);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isPrgrsSttsUnsaved) {
|
||||
alert(UNSAVED_PRGRS_STTS_MESSAGE);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleSavePrgrsStts() {
|
||||
if (!elaprDmndId) {
|
||||
alert("결재 요청 정보가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const validationError = validateAllPrgrsSttsRows(rows, isEditingLocked);
|
||||
if (validationError) {
|
||||
alert(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
const orderError = validateAtrzOrder(rows);
|
||||
if (orderError) {
|
||||
alert(orderError);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = buildPrgrsSttsSavePayload(rows, elaprDmndId, elaprDocPrgrsList ?? []);
|
||||
if (payload.length === 0) {
|
||||
alert("변경된 결재선이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
await savePrgrsSttsMutation.mutateAsync(payload);
|
||||
alert("결재선이 저장되었습니다.");
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND, "findDocPrgrsByDocNo", elaprDocNo],
|
||||
});
|
||||
onSuccess();
|
||||
} catch (error: unknown) {
|
||||
let errorMessage = "결재선 저장 중 오류가 발생했습니다.";
|
||||
if (error instanceof AxiosError) {
|
||||
errorMessage = error.response?.data?.message ?? errorMessage;
|
||||
}
|
||||
alert(errorMessage);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleApprove() {
|
||||
if (!currentUserPrgrs?.elaprPrgrsId) return;
|
||||
if (!assertPrgrsSttsSaved()) return;
|
||||
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
await elaprPrgrsSttsService.process(currentUserPrgrs.elaprPrgrsId, {
|
||||
atrzPrgrsSttsComCd: "ATRZ_PRGRS_003",
|
||||
finalApprovalYn,
|
||||
});
|
||||
alert("결재가 완료되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND, "findDocPrgrsByDocNo", elaprDocNo],
|
||||
});
|
||||
onSuccess();
|
||||
onOpenChange(false);
|
||||
} catch (error: unknown) {
|
||||
let errorMessage = "";
|
||||
if (error instanceof AxiosError) {
|
||||
errorMessage = error.response?.data?.message ?? "";
|
||||
} else {
|
||||
errorMessage = "결재 처리 중 오류가 발생했습니다.";
|
||||
}
|
||||
alert(errorMessage);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReject() {
|
||||
if (!currentUserPrgrs?.elaprPrgrsId) return;
|
||||
if (!rjctRsn.trim()) {
|
||||
alert("반려 사유를 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
if (!assertPrgrsSttsSaved()) return;
|
||||
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
await elaprPrgrsSttsService.process(currentUserPrgrs.elaprPrgrsId, {
|
||||
atrzPrgrsSttsComCd: "ATRZ_PRGRS_004",
|
||||
rjctRsn: rjctRsn.trim(),
|
||||
});
|
||||
alert("반려 처리가 완료되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND, "findDocPrgrsByDocNo", elaprDocNo],
|
||||
});
|
||||
onSuccess();
|
||||
onOpenChange(false);
|
||||
} catch (error: unknown) {
|
||||
let errorMessage = "";
|
||||
if (error instanceof AxiosError) {
|
||||
errorMessage = error.response?.data?.message ?? "";
|
||||
} else {
|
||||
errorMessage = "반려 처리 중 오류가 발생했습니다.";
|
||||
}
|
||||
alert(errorMessage);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
setRjctRsn("");
|
||||
setPickingRowKey(null);
|
||||
onOpenChange(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-[72rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>전자결재</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="mb-3 flex flex-wrap items-center justify-between gap-3">
|
||||
<h4 className="text-xl font-semibold">결재 진행 단계</h4>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
type="button"
|
||||
color="white"
|
||||
disabled={!elaprDmndId || isEditingLocked || isBusy}
|
||||
onClick={addRow}
|
||||
>
|
||||
결재선 추가
|
||||
</Button>
|
||||
{elaprDmndId && (
|
||||
<Button
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={handleSavePrgrsStts}
|
||||
disabled={!isPrgrsSttsUnsaved || isEditingLocked || isBusy}
|
||||
>
|
||||
결재선 저장
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-12 w-full" />
|
||||
<Skeleton className="h-32 w-full" />
|
||||
</div>
|
||||
) : visibleRows.length === 0 ? (
|
||||
<div className="py-8 text-center text-gray-500">
|
||||
{elaprDmndId
|
||||
? "등록된 결재선이 없습니다. [결재선 추가]로 결재자를 등록하세요."
|
||||
: "결재 정보가 없습니다."}
|
||||
</div>
|
||||
) : (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<Table className="w-full">
|
||||
<colgroup>
|
||||
<col width="8%" />
|
||||
<col width="10%" />
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
<col width="12%" />
|
||||
<col width="20%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-10" />
|
||||
<TableHead>결재순서</TableHead>
|
||||
<TableHead>결재구분</TableHead>
|
||||
<TableHead>결재자</TableHead>
|
||||
<TableHead>결재상태</TableHead>
|
||||
<TableHead>관리</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<SortableContext
|
||||
items={visibleRows.map((r) => r.rowKey)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{visibleRows.map((row, index) => (
|
||||
<SortablePrgrsSttsRow
|
||||
key={row.rowKey}
|
||||
row={row}
|
||||
displaySeqNo={index + 1}
|
||||
isEditing={editingRowKey === row.rowKey}
|
||||
isLocked={isEditingLocked}
|
||||
dragDisabled={
|
||||
isEditingLocked || !isPrgrsSttsRowEditable(row) || !row.atrzSeComCd
|
||||
}
|
||||
onStartEdit={() => startEdit(row.rowKey)}
|
||||
onSave={() => handleRowSave(row.rowKey)}
|
||||
onCancel={cancelEdit}
|
||||
onDelete={() => markDeleted(row.rowKey)}
|
||||
onChange={(patch) => updateRow(row.rowKey, patch)}
|
||||
onPickAprvr={() => {
|
||||
setPickingRowKey(row.rowKey);
|
||||
setPickerOpen(true);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</DndContext>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{canApprove && (
|
||||
<div>
|
||||
<h4 className="mb-3 text-xl font-semibold">반려 의견</h4>
|
||||
<Textarea
|
||||
value={rjctRsn}
|
||||
onChange={(event) => setRjctRsn(event.target.value)}
|
||||
placeholder="반려 시 사유를 입력하세요"
|
||||
rows={4}
|
||||
className="!text-xl"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<Button type="button" color="white" onClick={handleClose} disabled={isBusy}>
|
||||
취소
|
||||
</Button>
|
||||
{canApprove && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="mr-2 flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="all-approve"
|
||||
checked={finalApprovalYn}
|
||||
onCheckedChange={(checked) => setFinalApprovalYn(checked === true)}
|
||||
/>
|
||||
<Label htmlFor="all-approve" className="text-xl">
|
||||
전결
|
||||
</Label>
|
||||
</div>
|
||||
<Button onClick={handleApprove} disabled={isBusy}>
|
||||
결재
|
||||
</Button>
|
||||
<Button color="red" onClick={handleReject} disabled={isBusy}>
|
||||
반려
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
<AprvrPickerDialog
|
||||
open={pickerOpen}
|
||||
onOpenChange={setPickerOpen}
|
||||
atrzSeComCd={pickingRow?.atrzSeComCd ?? ""}
|
||||
onSelect={handleReviewerSelect}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { ElaprApprovalDialog };
|
||||
@@ -11,8 +11,11 @@ import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import {
|
||||
buildElaprPrgrsSttsListFromAplnRows,
|
||||
ElaprApprovalDialog,
|
||||
type ElaprDmndSaveRequestDto,
|
||||
elaprDmndService,
|
||||
type ElaprTmpltPickerConfirmPayload,
|
||||
ElaprTmpltPickerDialog,
|
||||
hasCurrentUserPendingApprovalRequest,
|
||||
useElaprDocPrgrsByDocNo,
|
||||
} from "@/entities/elapr";
|
||||
@@ -23,8 +26,7 @@ import {
|
||||
type FnrsCnsltnMngDetResponseDto,
|
||||
fnrsCnsltnMngService,
|
||||
} from "@/entities/fnrs-cnsltn";
|
||||
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
|
||||
import { TextInputDialog } from "@/widgets/pbadms-sprt";
|
||||
import { SplmntDmndDialog, type SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import {
|
||||
@@ -37,24 +39,20 @@ import { computeReviewStepChecklistProgress } from "../../lib/checklist-utils";
|
||||
import {
|
||||
buildSavePayload,
|
||||
buildSupplementReqPayload,
|
||||
buildSupplementSubmitPayload,
|
||||
buildTechReviewReplyPayload,
|
||||
toSplmntDmndSaveItem,
|
||||
} from "../../lib/save-payload-utils";
|
||||
import { type ChecklistFormState } from "../../model/view-types";
|
||||
import type { TechReviewOpinionSectionHandle } from "../sections/tech-review-opinion-section";
|
||||
|
||||
import { ElaprApprovalDialog } from "./elapr-approval-dialog";
|
||||
import {
|
||||
type ElaprTmpltPickerConfirmPayload,
|
||||
ElaprTmpltPickerDialog,
|
||||
} from "./elapr-tmplt-picker-dialog";
|
||||
|
||||
interface FloatingActionButtonsProps {
|
||||
data: FnrsCnsltnMngDetResponseDto;
|
||||
docSeComCd: string;
|
||||
fnrsCnsltnDocNo: string;
|
||||
prgrsSttsComCd: string;
|
||||
dtlInstCd: string | undefined;
|
||||
atrzSeComCdList: string[];
|
||||
isRvwLoginUserLoading: boolean;
|
||||
checklistFormState: ChecklistFormState;
|
||||
techReviewOpinionRef?: React.RefObject<TechReviewOpinionSectionHandle | null>;
|
||||
opnrNm?: string;
|
||||
@@ -68,6 +66,8 @@ function FloatingActionButtons({
|
||||
fnrsCnsltnDocNo,
|
||||
prgrsSttsComCd,
|
||||
dtlInstCd,
|
||||
atrzSeComCdList,
|
||||
isRvwLoginUserLoading,
|
||||
checklistFormState,
|
||||
techReviewOpinionRef,
|
||||
opnrNm,
|
||||
@@ -85,18 +85,9 @@ function FloatingActionButtons({
|
||||
|
||||
const needsAtrzResolve = needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd);
|
||||
const needsTeApprovalCheck = needsAtrzResolve && prgrsSttsComCd === "TE";
|
||||
const { data: rvwLoginUserData, isLoading: isRvwLoginUserLoading } = useRvwMnpwMngLoginUserList(
|
||||
{},
|
||||
{ enabled: needsAtrzResolve },
|
||||
);
|
||||
const { data: elaprDocPrgrsList = [], isLoading: isElaprDocPrgrsLoading } =
|
||||
useElaprDocPrgrsByDocNo(needsTeApprovalCheck ? docNo : null);
|
||||
|
||||
const atrzSeComCdList = useMemo(
|
||||
() => rvwLoginUserData?.content?.map((item) => item.atrzSeComCd) ?? [],
|
||||
[rvwLoginUserData],
|
||||
);
|
||||
|
||||
const hasPendingApprovalRequest = hasCurrentUserPendingApprovalRequest(
|
||||
elaprDocPrgrsList,
|
||||
userInfo?.userUnqId,
|
||||
@@ -222,15 +213,6 @@ function FloatingActionButtons({
|
||||
router.push("/fnrs-cnsltn");
|
||||
}
|
||||
|
||||
async function handleSplmntCmptnClick() {
|
||||
const payload = buildSupplementSubmitPayload(data, checklistFormState);
|
||||
await executeAction({
|
||||
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.splmntCmptn,
|
||||
request: payload,
|
||||
successMessage: "보완완료 처리되었습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
async function handleFnrsCnsltnAncmntClick() {
|
||||
const payload: DocPrgrsUpdateRequestDto = {
|
||||
docNo,
|
||||
@@ -383,15 +365,18 @@ function FloatingActionButtons({
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = buildTechReviewReplyPayload(
|
||||
data,
|
||||
checklistFormState,
|
||||
"KECO",
|
||||
"TE",
|
||||
"SV",
|
||||
getReviewOpinionFormData(),
|
||||
opnrNm,
|
||||
);
|
||||
const payload = {
|
||||
...buildTechReviewReplyPayload(
|
||||
data,
|
||||
checklistFormState,
|
||||
"KECO",
|
||||
"TE",
|
||||
"SV",
|
||||
getReviewOpinionFormData(),
|
||||
opnrNm,
|
||||
),
|
||||
splmntDmndList: (data.splmntDmndList ?? []).map(toSplmntDmndSaveItem),
|
||||
};
|
||||
|
||||
await executeAction({
|
||||
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn,
|
||||
@@ -405,12 +390,12 @@ function FloatingActionButtons({
|
||||
setActiveDialog("dmndSplmnt");
|
||||
}
|
||||
|
||||
async function handleDmndSplmntConfirm(splmntDmndCn: string) {
|
||||
async function handleDmndSplmntConfirm(payload: SplmntDmndDialogConfirmPayload) {
|
||||
setActiveDialog(null);
|
||||
const payload = buildSupplementReqPayload(data, splmntDmndCn);
|
||||
const requestPayload = buildSupplementReqPayload(data, payload, checklistFormState);
|
||||
await executeAction({
|
||||
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt,
|
||||
request: payload,
|
||||
request: requestPayload,
|
||||
successMessage: "보완요청이 처리되었습니다.",
|
||||
});
|
||||
}
|
||||
@@ -442,11 +427,6 @@ function FloatingActionButtons({
|
||||
삭제
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.splmntCmptn) && (
|
||||
<Button onClick={handleSplmntCmptnClick} disabled={isProcessing}>
|
||||
보완완료
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.fnrsCnsltnAncmnt) && (
|
||||
<Button onClick={handleFnrsCnsltnAncmntClick} disabled={isProcessing}>
|
||||
재원협의 고시
|
||||
@@ -513,14 +493,13 @@ function FloatingActionButtons({
|
||||
<ElaprTmpltPickerDialog
|
||||
open={activeDialog === "elaprTmplt"}
|
||||
onClose={() => setActiveDialog(null)}
|
||||
docSeComCd={docSeComCd}
|
||||
onConfirm={handleElaprTmpltConfirm}
|
||||
/>
|
||||
|
||||
<TextInputDialog
|
||||
<SplmntDmndDialog
|
||||
open={activeDialog === "dmndSplmnt"}
|
||||
onOpenChange={() => setActiveDialog(null)}
|
||||
title="보완요청 내용 입력"
|
||||
placeholder="보완요청 내용을 입력하세요."
|
||||
isSubmitting={isProcessing}
|
||||
onConfirm={handleDmndSplmntConfirm}
|
||||
/>
|
||||
@@ -562,19 +541,6 @@ async function handleDel(context: ActionHandlerContext): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSplmntCmptn(
|
||||
context: ActionHandlerContext<FnrsCnsltnApplySaveRequestDto>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const docNo = context.data.detDto?.fnrsCnsltnDocNo;
|
||||
if (!docNo) throw new Error("문서번호가 없습니다.");
|
||||
await fnrsCnsltnMngService.submitSupplement(docNo, context.request);
|
||||
context.onSuccess?.();
|
||||
} catch (error) {
|
||||
context.onError?.(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleChangePrgrsSttsComCd(
|
||||
context: ActionHandlerContext<DocPrgrsUpdateRequestDto>,
|
||||
): Promise<void> {
|
||||
@@ -634,16 +600,10 @@ async function handleAtrzRply(context: ActionHandlerContext): Promise<void> {
|
||||
context.onSuccess?.();
|
||||
}
|
||||
|
||||
async function handleGrndsChck(context: ActionHandlerContext): Promise<void> {
|
||||
// TODO: 현장점검 등록 API 호출
|
||||
context.onSuccess?.();
|
||||
}
|
||||
|
||||
const buttonActionHandlers: Record<FnrsCnsltnViewButtonAction, ActionHandler> = {
|
||||
strg: handleSaveApply,
|
||||
aply: handleSaveApply,
|
||||
del: handleDel,
|
||||
splmntCmptn: handleSplmntCmptn,
|
||||
fnrsCnsltnAncmnt: handleChangePrgrsSttsComCd,
|
||||
envoSbmsn: handleChangePrgrsSttsComCd,
|
||||
rjct: handleChangePrgrsSttsComCd,
|
||||
@@ -656,7 +616,6 @@ const buttonActionHandlers: Record<FnrsCnsltnViewButtonAction, ActionHandler> =
|
||||
dmndSplmnt: handleDmndSplmnt,
|
||||
atrzAprv: handleAtrzAprv,
|
||||
atrzRply: handleAtrzRply,
|
||||
grndsChck: handleGrndsChck,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
"use client";
|
||||
|
||||
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";
|
||||
|
||||
interface SortableAplnPickerRowProps {
|
||||
row: ElaprAplnRow;
|
||||
displaySeqNo: number;
|
||||
isEditing: boolean;
|
||||
isLocked: boolean;
|
||||
dragDisabled: boolean;
|
||||
onStartEdit: () => void;
|
||||
onSave: () => void;
|
||||
onCancel: () => void;
|
||||
onDelete: () => void;
|
||||
onChange: (patch: Partial<ElaprAplnRow>) => void;
|
||||
onPickAprvr: () => void;
|
||||
}
|
||||
|
||||
function SortableAplnPickerRow({
|
||||
row,
|
||||
displaySeqNo,
|
||||
isEditing,
|
||||
isLocked,
|
||||
dragDisabled,
|
||||
onStartEdit,
|
||||
onSave,
|
||||
onCancel,
|
||||
onDelete,
|
||||
onChange,
|
||||
onPickAprvr,
|
||||
}: SortableAplnPickerRowProps) {
|
||||
const { data: atrzSeCodes } = useCodeList("ATRZSE");
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: row.rowKey,
|
||||
disabled: dragDisabled,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.6 : 1,
|
||||
};
|
||||
|
||||
const rowDisabled = isLocked && !isEditing;
|
||||
const atrzSeLabel = row.atrzSeComNm || findCodeLabel(atrzSeCodes, row.atrzSeComCd);
|
||||
|
||||
return (
|
||||
<TableRow ref={setNodeRef} style={style} className={isEditing ? "bg-accent/30" : undefined}>
|
||||
<TableCell align="center" className="w-10">
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
className="cursor-grab !bg-transparent text-muted-foreground disabled:cursor-not-allowed disabled:opacity-40"
|
||||
disabled={dragDisabled}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
aria-label="순서 변경"
|
||||
>
|
||||
<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]"
|
||||
value={row.atrzSeComCd ?? ""}
|
||||
onValueChange={(v) =>
|
||||
onChange({
|
||||
atrzSeComCd: v,
|
||||
atrzSeComNm: findCodeLabel(atrzSeCodes, v),
|
||||
})
|
||||
}
|
||||
placeholder="결재구분"
|
||||
/>
|
||||
) : (
|
||||
atrzSeLabel || "-"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<span>{row.aprvrNm || row.aprvrUuid || "-"}</span>
|
||||
{isEditing && (
|
||||
<Button type="button" size="sm" onClick={onPickAprvr}>
|
||||
선택
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<div className="flex justify-center gap-2">
|
||||
{isEditing ? (
|
||||
<>
|
||||
<Button type="button" size="sm" onClick={onSave}>
|
||||
확인
|
||||
</Button>
|
||||
<Button type="button" size="sm" color="white" onClick={onCancel}>
|
||||
취소
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="white"
|
||||
disabled={rowDisabled}
|
||||
onClick={onStartEdit}
|
||||
>
|
||||
수정
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="gray"
|
||||
disabled={rowDisabled}
|
||||
onClick={onDelete}
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export { SortableAplnPickerRow };
|
||||
@@ -11,6 +11,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import { useAcntgSeMngTree } from "@/entities/acntg";
|
||||
import {
|
||||
type FnrsCnsltnApplySaveRequestDto,
|
||||
fnrsCnsltnMngBaseKey,
|
||||
type FnrsCnsltnMngDetResponseDto,
|
||||
fnrsCnsltnMngService,
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
isFormChecklistComplete,
|
||||
PrivacyConsentSection,
|
||||
recalculateFnrsSharTotals,
|
||||
SplmntDmndFormSection,
|
||||
toFnrsSharSaveList,
|
||||
validateBizCstSharBalance,
|
||||
} from "@/widgets/pbadms-sprt";
|
||||
@@ -56,6 +58,7 @@ function FnrsFormPage({ id, mode, initialData, taskSeComCd }: FnrsFormPageProps)
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const isEditMode = mode === "edit";
|
||||
const isSplmntPrgrsStts = initialData?.detDto?.prgrsSttsComCd === "SL";
|
||||
|
||||
const { data: masterSharTree = [] } = useFnrsSharSeMngTree();
|
||||
const { data: acntgTree = [] } = useAcntgSeMngTree();
|
||||
@@ -73,10 +76,10 @@ function FnrsFormPage({ id, mode, initialData, taskSeComCd }: FnrsFormPageProps)
|
||||
: getCreateDefaultValues(userInfo, taskSeComCd),
|
||||
});
|
||||
|
||||
async function submitForm(
|
||||
function buildFormPayload(
|
||||
data: FnrsCnsltnRegistFormData,
|
||||
prgrsSttsComCd: "SV" | "ST",
|
||||
): Promise<string | undefined> {
|
||||
prgrsSttsComCd: "SV" | "ST" | "TE",
|
||||
): FnrsCnsltnApplySaveRequestDto | undefined {
|
||||
const withTotals = recalculateFnrsSharTotals(data.fnrsSharList, sharItems);
|
||||
const validationMessage = validateBizCstSharBalance(
|
||||
data.bizCstList,
|
||||
@@ -86,12 +89,12 @@ function FnrsFormPage({ id, mode, initialData, taskSeComCd }: FnrsFormPageProps)
|
||||
);
|
||||
if (validationMessage) {
|
||||
alert(validationMessage);
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { taskSeComCd: formTaskSeComCd, ...fnrsCnsltnMngRest } = data.fnrsCnsltnMng;
|
||||
|
||||
const payload = {
|
||||
return {
|
||||
fnrsCnsltnMng: {
|
||||
...fnrsCnsltnMngRest,
|
||||
taskSeComCd: formatTaskSeComCdForApi(formTaskSeComCd),
|
||||
@@ -106,11 +109,71 @@ function FnrsFormPage({ id, mode, initialData, taskSeComCd }: FnrsFormPageProps)
|
||||
selected: item.selected ?? undefined,
|
||||
})),
|
||||
esntlAtchFileList: data.esntlAtchFileList,
|
||||
splmntDmndList: data.splmntDmndList.map(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
({ splmntAtchFileNm, splmntAtchFileAtflId, splmntDmndAtchFileNm, ...item }) => ({
|
||||
...item,
|
||||
...(prgrsSttsComCd === "TE" ? { cmptnYn: "Y" } : null),
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async function submitForm(
|
||||
data: FnrsCnsltnRegistFormData,
|
||||
prgrsSttsComCd: "SV" | "ST",
|
||||
): Promise<string | undefined> {
|
||||
const payload = buildFormPayload(data, prgrsSttsComCd);
|
||||
if (!payload) return undefined;
|
||||
return fnrsCnsltnMngService.saveApply(payload);
|
||||
}
|
||||
|
||||
async function submitSupplementForm(data: FnrsCnsltnRegistFormData): Promise<string | undefined> {
|
||||
const docNo = initialData?.detDto?.fnrsCnsltnDocNo;
|
||||
if (!docNo) return undefined;
|
||||
|
||||
const payload = buildFormPayload(data, "TE");
|
||||
if (!payload) return undefined;
|
||||
|
||||
return fnrsCnsltnMngService.submitSupplement(docNo, payload);
|
||||
}
|
||||
|
||||
async function onSplmntCmptn(data: FnrsCnsltnRegistFormData) {
|
||||
if (isSubmitting) return;
|
||||
|
||||
const hasEmptySplmntCn = data.splmntDmndList?.some(
|
||||
(item) => !item.splmntCn || !item.splmntCn.trim(),
|
||||
);
|
||||
if (hasEmptySplmntCn) {
|
||||
alert("보완요청내용이 있습니다. 보완완료내용을 입력해 주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const docNo = initialData?.detDto?.fnrsCnsltnDocNo;
|
||||
if (!docNo) {
|
||||
alert("문서번호가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const result = await submitSupplementForm(data);
|
||||
if (!result) return;
|
||||
|
||||
alert("보완완료 처리되었습니다.");
|
||||
router.push(`/fnrs-cnsltn/${docNo}`);
|
||||
queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...fnrsCnsltnMngBaseKey, "findById", docNo],
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("보완완료 실패:", error);
|
||||
alert("보완완료 처리 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onSave(data: FnrsCnsltnRegistFormData) {
|
||||
if (isSubmitting) return;
|
||||
|
||||
@@ -210,6 +273,7 @@ function FnrsFormPage({ id, mode, initialData, taskSeComCd }: FnrsFormPageProps)
|
||||
initialEsntlFiles={isEditMode ? initialData?.esntlFileList : undefined}
|
||||
initialEtcFiles={isEditMode ? initialData?.etcFileList : undefined}
|
||||
/>
|
||||
<SplmntDmndFormSection readOnly={!isSplmntPrgrsStts} />
|
||||
|
||||
<div className="sticky bottom-0 z-40 !-mb-2 border-t bg-white pb-8 pt-6 shadow-[0_-4px_12px_rgba(0,0,0,0.06)]">
|
||||
<div className="mx-4 flex flex-wrap justify-center gap-3 sm:gap-4">
|
||||
@@ -221,23 +285,40 @@ function FnrsFormPage({ id, mode, initialData, taskSeComCd }: FnrsFormPageProps)
|
||||
>
|
||||
목록
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
저장
|
||||
</Button>
|
||||
{isEditMode && (
|
||||
{!isSplmntPrgrsStts ? (
|
||||
<>
|
||||
<Button
|
||||
color="green"
|
||||
type="button"
|
||||
onClick={handleApplyClick}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
신청완료
|
||||
</Button>
|
||||
<Button color="red" type="button" onClick={handleDelClick} disabled={isSubmitting}>
|
||||
삭제
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
저장
|
||||
</Button>
|
||||
{isEditMode && (
|
||||
<>
|
||||
<Button
|
||||
color="green"
|
||||
type="button"
|
||||
onClick={handleApplyClick}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
신청완료
|
||||
</Button>
|
||||
<Button
|
||||
color="red"
|
||||
type="button"
|
||||
onClick={handleDelClick}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={form.handleSubmit(onSplmntCmptn)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
보완완료
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import { useMemo, useRef } from "react";
|
||||
|
||||
import { useUserStore } from "@/shared/lib";
|
||||
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
|
||||
import { useFnrsCnsltnMng } from "@/entities/fnrs-cnsltn";
|
||||
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
|
||||
import { SectionCollapsible, SupplementDemandSection } from "@/widgets/pbadms-sprt";
|
||||
import { Skeleton } from "@common/components";
|
||||
|
||||
import { needsRoleUser01AtrzResolve } from "../lib/button-config";
|
||||
import { getEditableReviewStep } from "../lib/review-step-utils";
|
||||
import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-checklist-form-state";
|
||||
|
||||
@@ -17,7 +21,6 @@ import { BusinessScaleSection } from "./sections/business-scale-section";
|
||||
import { ChecklistSection } from "./sections/checklist-section";
|
||||
import { ContactInfoSection } from "./sections/contact-info-section";
|
||||
import { FnrsSharSection } from "./sections/fnrs-shar-section";
|
||||
import { SupplementDemandSection } from "./sections/supplement-demand-section";
|
||||
import {
|
||||
TechReviewOpinionSection,
|
||||
type TechReviewOpinionSectionHandle,
|
||||
@@ -34,6 +37,17 @@ function FnrsViewPage({ id }: FnrsViewPageProps) {
|
||||
const { formState: checklistFormState, setFormState: setChecklistFormState } =
|
||||
useChecklistFormState(data?.cklstResultList ?? EMPTY_CKLST_RESULT_LIST);
|
||||
|
||||
const prgrsSttsComCd = data?.detDto?.prgrsSttsComCd || "";
|
||||
const needsAtrzResolve = needsRoleUser01AtrzResolve(userInfo?.dtlInstCd, prgrsSttsComCd);
|
||||
const { data: rvwLoginUserData, isLoading: isRvwLoginUserLoading } = useRvwMnpwMngLoginUserList(
|
||||
{},
|
||||
{ enabled: needsAtrzResolve },
|
||||
);
|
||||
const atrzSeComCdList = useMemo(
|
||||
() => rvwLoginUserData?.content?.map((item) => item.atrzSeComCd) ?? [],
|
||||
[rvwLoginUserData],
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="mt-6 flex flex-col gap-4 px-6">
|
||||
@@ -76,6 +90,9 @@ function FnrsViewPage({ id }: FnrsViewPageProps) {
|
||||
<div className="mt-6 space-y-4">
|
||||
<BasicInfoSection det={detDto} />
|
||||
<ContactInfoSection det={detDto} />
|
||||
<SectionCollapsible title="결재현황">
|
||||
<ElaprDocPrgrsStatus elaprDocNo={detDto.fnrsCnsltnDocNo} />
|
||||
</SectionCollapsible>
|
||||
<BusinessScaleSection bizSclList={bizSclList} />
|
||||
<BusinessCostSection bizCstList={bizCstList} />
|
||||
<FnrsSharSection fnrsSharList={fnrsSharList} />
|
||||
@@ -92,6 +109,8 @@ function FnrsViewPage({ id }: FnrsViewPageProps) {
|
||||
ref={techReviewOpinionRef}
|
||||
rvwList={rvwList}
|
||||
editableReviewStep={editableReviewStep}
|
||||
atrzSeComCdList={atrzSeComCdList}
|
||||
isAtrzLoading={needsAtrzResolve && isRvwLoginUserLoading}
|
||||
/>
|
||||
<FloatingActionButtons
|
||||
data={data}
|
||||
@@ -99,6 +118,8 @@ function FnrsViewPage({ id }: FnrsViewPageProps) {
|
||||
fnrsCnsltnDocNo={detDto.fnrsCnsltnDocNo}
|
||||
prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
|
||||
dtlInstCd={userInfo?.dtlInstCd}
|
||||
atrzSeComCdList={atrzSeComCdList}
|
||||
isRvwLoginUserLoading={needsAtrzResolve && isRvwLoginUserLoading}
|
||||
checklistFormState={checklistFormState}
|
||||
techReviewOpinionRef={techReviewOpinionRef}
|
||||
opnrNm={userInfo?.userName}
|
||||
|
||||
@@ -12,6 +12,12 @@ function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>사업명</TableHead>
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { dayjs } from "@/shared/lib";
|
||||
import type { FnrsCnsltnSplmntDmndResponseDto } from "@/entities/fnrs-cnsltn";
|
||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
import {
|
||||
Badge,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
interface SupplementDemandSectionProps {
|
||||
splmntDmndList: FnrsCnsltnSplmntDmndResponseDto[];
|
||||
}
|
||||
|
||||
function SupplementDemandSection({ splmntDmndList }: SupplementDemandSectionProps) {
|
||||
if (splmntDmndList.length === 0) return null;
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="보완요청">
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[8rem]">항목</TableHead>
|
||||
<TableHead>보완요청 내용</TableHead>
|
||||
<TableHead>보완 내용</TableHead>
|
||||
<TableHead className="w-[8rem]">완료여부</TableHead>
|
||||
<TableHead className="w-[12rem]">요청일시</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{splmntDmndList.map((item) => (
|
||||
<TableRow key={item.splmntDmndSn}>
|
||||
<TableCell>{item.splmntArtclCdNm || "-"}</TableCell>
|
||||
<TableCell className="whitespace-pre-wrap">{item.splmntDmndCn || "-"}</TableCell>
|
||||
<TableCell className="whitespace-pre-wrap">{item.splmntCn || "-"}</TableCell>
|
||||
<TableCell>
|
||||
<Badge color={item.cmptnYn === "Y" ? "green" : "gray"}>
|
||||
{item.cmptnYn === "Y" ? "완료" : "미완료"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{item.splmntDmndDt ? dayjs(item.splmntDmndDt).format("YYYY-MM-DD HH:mm") : "-"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</SectionCollapsible>
|
||||
);
|
||||
}
|
||||
|
||||
export { SupplementDemandSection };
|
||||
@@ -33,6 +33,8 @@ const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
||||
interface TechReviewOpinionSectionProps {
|
||||
rvwList: FnrsCnsltnRvwOpnnResponseDto[];
|
||||
editableReviewStep?: ReviewStepCode;
|
||||
atrzSeComCdList?: string[];
|
||||
isAtrzLoading?: boolean;
|
||||
}
|
||||
|
||||
export interface TechReviewOpinionSectionHandle {
|
||||
@@ -69,7 +71,13 @@ function hasFormContent(formData: ReviewOpinionFormData) {
|
||||
const TechReviewOpinionSection = forwardRef<
|
||||
TechReviewOpinionSectionHandle,
|
||||
TechReviewOpinionSectionProps
|
||||
>(function TechReviewOpinionSection({ rvwList, editableReviewStep }, ref) {
|
||||
>(function TechReviewOpinionSection({
|
||||
rvwList,
|
||||
editableReviewStep,
|
||||
atrzSeComCdList = [],
|
||||
isAtrzLoading = false,
|
||||
}, ref) {
|
||||
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||
...EMPTY_FORM_DATA,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
@@ -102,6 +110,9 @@ const TechReviewOpinionSection = forwardRef<
|
||||
if (!editableReviewStep || !hasFormContent(formData)) {
|
||||
return null;
|
||||
}
|
||||
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
formData: {
|
||||
@@ -119,14 +130,15 @@ const TechReviewOpinionSection = forwardRef<
|
||||
setUploadedFile(null);
|
||||
},
|
||||
}),
|
||||
[editableReviewStep, formData, rvwList],
|
||||
[canWriteKecoOpinion, editableReviewStep, formData, rvwList],
|
||||
);
|
||||
|
||||
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
|
||||
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
||||
|
||||
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
|
||||
const hasKecoSection = kecoRvwList.length > 0 || editableReviewStep === "KECO";
|
||||
const hasKecoSection =
|
||||
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteKecoOpinion);
|
||||
|
||||
if (!hasEnvSection && !hasKecoSection) {
|
||||
return null;
|
||||
@@ -170,6 +182,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
|
||||
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
||||
if (editableReviewStep !== reviewStep) return null;
|
||||
if (reviewStep === "KECO" && !canWriteKecoOpinion) return null;
|
||||
|
||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import type { ElaprDocPrgrsClientResponseDto } from "@/entities/instl-sncn";
|
||||
|
||||
export interface ApproverStatusConfig {
|
||||
label: string;
|
||||
badgeClass: string;
|
||||
}
|
||||
|
||||
export const APPROVER_STATUS_MAP: Record<string, ApproverStatusConfig> = {
|
||||
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" },
|
||||
};
|
||||
|
||||
export function sortByAtrzSeqNo(
|
||||
list: ElaprDocPrgrsClientResponseDto[],
|
||||
): ElaprDocPrgrsClientResponseDto[] {
|
||||
return list.slice().sort((a, b) => Number(a.atrzSeqNo ?? 0) - Number(b.atrzSeqNo ?? 0));
|
||||
}
|
||||
|
||||
export function getApproverStatus(status: string | undefined): ApproverStatusConfig {
|
||||
return (
|
||||
APPROVER_STATUS_MAP[status ?? ""] ?? {
|
||||
label: status || "-",
|
||||
badgeClass: "bg-gray-100 text-gray-700",
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,6 @@ export const INSTL_SNCN_VIEW_BUTTON_ACTION = {
|
||||
strg: "strg",
|
||||
aply: "aply",
|
||||
del: "del",
|
||||
splmntCmptn: "splmntCmptn",
|
||||
instlSncnAncmnt: "instlSncnAncmnt",
|
||||
envoSbmsn: "envoSbmsn",
|
||||
rjct: "rjct",
|
||||
@@ -15,7 +14,6 @@ export const INSTL_SNCN_VIEW_BUTTON_ACTION = {
|
||||
dmndSplmnt: "dmndSplmnt",
|
||||
atrzAprv: "atrzAprv",
|
||||
atrzRply: "atrzRply",
|
||||
grndsChck: "grndsChck",
|
||||
} as const;
|
||||
|
||||
export type InstlSncnViewButtonAction =
|
||||
@@ -33,7 +31,6 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, InstlSncnViewBut
|
||||
INSTL_SNCN_VIEW_BUTTON_ACTION.aply,
|
||||
INSTL_SNCN_VIEW_BUTTON_ACTION.del,
|
||||
],
|
||||
SL: [INSTL_SNCN_VIEW_BUTTON_ACTION.splmntCmptn],
|
||||
TA: [INSTL_SNCN_VIEW_BUTTON_ACTION.instlSncnAncmnt],
|
||||
},
|
||||
ROLE_USER04: {
|
||||
@@ -44,10 +41,6 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, InstlSncnViewBut
|
||||
LE: [INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply, INSTL_SNCN_VIEW_BUTTON_ACTION.rqstTechRvw],
|
||||
TP: [INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply],
|
||||
},
|
||||
ROLE_USER01: {
|
||||
TE: [INSTL_SNCN_VIEW_BUTTON_ACTION.grndsChck],
|
||||
SL: [INSTL_SNCN_VIEW_BUTTON_ACTION.splmntCmptn],
|
||||
},
|
||||
};
|
||||
|
||||
const ROLE_USER01_ATRZ_STATUS_CODES = ["ER", "TE"] as const;
|
||||
|
||||
@@ -24,7 +24,7 @@ function mapEsntlAtchFileList(esntlFileList: InstlSncnMngDetResponse["esntlFileL
|
||||
}
|
||||
|
||||
export function mapDetailToFormData(data: InstlSncnMngDetResponse): InstlSncnRegistFormData {
|
||||
const { detDto, bizSclList, bizCstList, cklstResultList, esntlFileList } = data;
|
||||
const { detDto, bizSclList, bizCstList, cklstResultList, esntlFileList, splmntDmndList } = data;
|
||||
|
||||
return {
|
||||
instlSncnMng: {
|
||||
@@ -85,6 +85,23 @@ export function mapDetailToFormData(data: InstlSncnMngDetResponse): InstlSncnReg
|
||||
picRvwCn: item.picRvwCn,
|
||||
})) ?? [],
|
||||
esntlAtchFileList: mapEsntlAtchFileList(esntlFileList),
|
||||
splmntDmndList:
|
||||
splmntDmndList?.map((item) => ({
|
||||
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",
|
||||
};
|
||||
}
|
||||
@@ -116,6 +133,7 @@ export function getCreateDefaultValues(
|
||||
bizCstList: [],
|
||||
cklstResultList: [],
|
||||
esntlAtchFileList: [],
|
||||
splmntDmndList: [],
|
||||
privacyConsent: "N",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,23 +24,23 @@ export const instlSncnRegistSchema = z.object({
|
||||
aplcntMblTelno: z.string().optional(),
|
||||
aplcntOgdpNm: z.string().optional(),
|
||||
aplcntEmlAddr: z.string().optional(),
|
||||
instlSncnDocNo: z.string().optional(),
|
||||
instlSncnDocNo: z.string().nullish(),
|
||||
admdstCd: z.string().optional(),
|
||||
envoSeComCd: z.string().optional(),
|
||||
prgrsSttsComCd: z.string().optional(),
|
||||
prgrsSttsComCd: z.string().nullish(),
|
||||
atchFileGroupId: z.string().optional(),
|
||||
rtlztnFcltSeComCd: z.string().optional(),
|
||||
rtlztnUsgSeComCd: z.string().optional(),
|
||||
envoSeCdNm: z.string().optional(),
|
||||
sncnSeCdNm: z.string().optional(),
|
||||
taskSeCdNm: z.string().optional(),
|
||||
prgrsSttsCdNm: z.string().optional(),
|
||||
sido: z.string().optional(),
|
||||
gugun: z.string().optional(),
|
||||
regDt: z.string().optional(),
|
||||
rgtr: z.string().optional(),
|
||||
rgtrNm: z.string().optional(),
|
||||
mdfcnDt: z.string().optional(),
|
||||
envoSeCdNm: z.string().nullish(),
|
||||
sncnSeCdNm: z.string().nullish(),
|
||||
taskSeCdNm: z.string().nullish(),
|
||||
prgrsSttsCdNm: z.string().nullish(),
|
||||
sido: z.string().nullish(),
|
||||
gugun: z.string().nullish(),
|
||||
regDt: z.string().nullish(),
|
||||
rgtr: z.string().nullish(),
|
||||
rgtrNm: z.string().nullish(),
|
||||
mdfcnDt: z.string().nullish(),
|
||||
}),
|
||||
bzScaleList: z.array(
|
||||
z.object({
|
||||
@@ -66,7 +66,7 @@ export const instlSncnRegistSchema = z.object({
|
||||
cklstRvwSn: z.number(),
|
||||
rvwStpComCd: z.string(),
|
||||
selected: z.enum(["Y", "N"]).nullable(),
|
||||
picRvwCn: z.string().nullable().optional(),
|
||||
picRvwCn: z.string().nullish(),
|
||||
}),
|
||||
),
|
||||
esntlAtchFileList: z.array(
|
||||
@@ -75,6 +75,24 @@ export const instlSncnRegistSchema = z.object({
|
||||
atchFileGroupId: z.string(),
|
||||
}),
|
||||
),
|
||||
splmntDmndList: z.array(
|
||||
z.object({
|
||||
splmntDmndSn: z.number().optional(),
|
||||
splmntArtclComCd: z.string().optional(),
|
||||
splmntArtclCdNm: z.string().nullish(),
|
||||
splmntDmndCn: z.string().optional(),
|
||||
splmntDmndDt: z.string().optional(),
|
||||
splmntDmndAtchFileGroupId: z.string().optional(),
|
||||
splmntDmndAtchFileNm: z.string().optional(),
|
||||
splmntCn: z.string().optional(),
|
||||
splmntPicNm: z.string().optional(),
|
||||
splmntCmptnDt: z.string().optional(),
|
||||
splmntAtchFileGroupId: z.string().optional(),
|
||||
splmntAtchFileNm: z.string().optional(),
|
||||
splmntAtchFileAtflId: z.string().optional(),
|
||||
cmptnYn: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
privacyConsent: z.enum(["Y", "N"], {
|
||||
required_error: "개인정보 수집/이용 동의는 필수입니다",
|
||||
}),
|
||||
|
||||
@@ -1,104 +1,131 @@
|
||||
import type {
|
||||
BizCstTreeDto,
|
||||
BizSclTreeDto,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
InstlSncnApplySaveRequestDto,
|
||||
InstlSncnBizCstSaveRequestDto,
|
||||
InstlSncnBzScaleSaveDto,
|
||||
InstlSncnCklstResultSaveDto,
|
||||
InstlSncnEsntlAtchFileSaveDto,
|
||||
InstlSncnMngDetResponse,
|
||||
InstlSncnMngResponseDto,
|
||||
ReviewStepCode,
|
||||
} from "@/entities/instl-sncn";
|
||||
|
||||
import type { ChecklistFormState } from "../model/view-types";
|
||||
|
||||
import { toCklstSaveDto } from "./checklist-utils";
|
||||
import { type ReviewOpinionFormData, toRvwOpnnSaveDto } from "./review-opinion-utils";
|
||||
|
||||
export function toBzScaleSaveDto(bizSclList: BizSclTreeDto[]): InstlSncnBzScaleSaveDto[] {
|
||||
return bizSclList
|
||||
.filter((node) => node.useYn === "Y")
|
||||
.map((node) => ({
|
||||
cnstpSeCd: node.cnstpSeCd,
|
||||
bscPl: node.bscPlanQty,
|
||||
budgetReflct: node.bgtRfltQty,
|
||||
thtiDsgn: node.tstmDesignQty,
|
||||
diff: node.tstmDesignQty - node.bgtRfltQty,
|
||||
}));
|
||||
}
|
||||
|
||||
export function toBizCstSaveDto(bizCstList: BizCstTreeDto[]): InstlSncnBizCstSaveRequestDto[] {
|
||||
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[],
|
||||
): InstlSncnEsntlAtchFileSaveDto[] {
|
||||
const esntlMap = new Map<string, string>();
|
||||
|
||||
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: InstlSncnMngDetResponse,
|
||||
checklistFormState: ChecklistFormState,
|
||||
reviewStep: ReviewStepCode,
|
||||
prgrsSttsComCd: string,
|
||||
): InstlSncnApplySaveRequestDto {
|
||||
const { detDto, bizSclList, bizCstList, cklstResultList = [], esntlFileList } = data;
|
||||
|
||||
return {
|
||||
instlSncnMng: { ...detDto, prgrsSttsComCd },
|
||||
bzScaleList: toBzScaleSaveDto(bizSclList),
|
||||
bizCstList: toBizCstSaveDto(bizCstList),
|
||||
cklstResultList: toCklstSaveDto(cklstResultList, checklistFormState, reviewStep),
|
||||
esntlAtchFileList: toEsntlAtchFileSaveDto(esntlFileList),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTechReviewReplyPayload(
|
||||
data: InstlSncnMngDetResponse,
|
||||
checklistFormState: ChecklistFormState,
|
||||
reviewStep: ReviewStepCode,
|
||||
prgrsSttsComCd: string,
|
||||
techReViewSaveCd: string,
|
||||
reviewOpinionFormData?: ReviewOpinionFormData | null,
|
||||
opnrNm?: string,
|
||||
): InstlSncnApplySaveRequestDto {
|
||||
return {
|
||||
...buildSavePayload(data, checklistFormState, reviewStep, prgrsSttsComCd),
|
||||
techReViewSaveCd,
|
||||
rvwStpCd: reviewStep,
|
||||
rvwOpnnList: toRvwOpnnSaveDto(data, reviewOpinionFormData, opnrNm),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTechReviewPayload(
|
||||
det: InstlSncnMngResponseDto,
|
||||
cklstResultList: InstlSncnCklstResultSaveDto[],
|
||||
): InstlSncnApplySaveRequestDto {
|
||||
return {
|
||||
instlSncnMng: { ...det, prgrsSttsComCd: "ER" },
|
||||
bzScaleList: [],
|
||||
bizCstList: [],
|
||||
cklstResultList,
|
||||
esntlAtchFileList: [],
|
||||
};
|
||||
}
|
||||
import type {
|
||||
BizCstTreeDto,
|
||||
BizSclTreeDto,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
InstlSncnApplySaveRequestDto,
|
||||
InstlSncnBizCstSaveRequestDto,
|
||||
InstlSncnBzScaleSaveDto,
|
||||
InstlSncnEsntlAtchFileSaveDto,
|
||||
InstlSncnMngDetResponse,
|
||||
InstlSncnSplmntDmndSaveDto,
|
||||
ReviewStepCode,
|
||||
} from "@/entities/instl-sncn";
|
||||
import type { SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt";
|
||||
|
||||
import type { ChecklistFormState } from "../model/view-types";
|
||||
|
||||
import { toCklstSaveDto } from "./checklist-utils";
|
||||
import { type ReviewOpinionFormData, toRvwOpnnSaveDto } from "./review-opinion-utils";
|
||||
|
||||
export function toBzScaleSaveDto(bizSclList: BizSclTreeDto[]): InstlSncnBzScaleSaveDto[] {
|
||||
return bizSclList
|
||||
.filter((node) => node.useYn === "Y")
|
||||
.map((node) => ({
|
||||
cnstpSeCd: node.cnstpSeCd,
|
||||
bscPl: node.bscPlanQty,
|
||||
budgetReflct: node.bgtRfltQty,
|
||||
thtiDsgn: node.tstmDesignQty,
|
||||
diff: node.tstmDesignQty - node.bgtRfltQty,
|
||||
}));
|
||||
}
|
||||
|
||||
export function toBizCstSaveDto(bizCstList: BizCstTreeDto[]): InstlSncnBizCstSaveRequestDto[] {
|
||||
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[],
|
||||
): InstlSncnEsntlAtchFileSaveDto[] {
|
||||
const esntlMap = new Map<string, string>();
|
||||
|
||||
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: InstlSncnMngDetResponse,
|
||||
checklistFormState: ChecklistFormState,
|
||||
reviewStep: ReviewStepCode,
|
||||
prgrsSttsComCd: string,
|
||||
): InstlSncnApplySaveRequestDto {
|
||||
const { detDto, bizSclList, bizCstList, cklstResultList = [], esntlFileList } = data;
|
||||
|
||||
return {
|
||||
instlSncnMng: { ...detDto, prgrsSttsComCd },
|
||||
bzScaleList: toBzScaleSaveDto(bizSclList),
|
||||
bizCstList: toBizCstSaveDto(bizCstList),
|
||||
cklstResultList: toCklstSaveDto(cklstResultList, checklistFormState, reviewStep),
|
||||
esntlAtchFileList: toEsntlAtchFileSaveDto(esntlFileList),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTechReviewReplyPayload(
|
||||
data: InstlSncnMngDetResponse,
|
||||
checklistFormState: ChecklistFormState,
|
||||
reviewStep: ReviewStepCode,
|
||||
prgrsSttsComCd: string,
|
||||
techReViewSaveCd: string,
|
||||
reviewOpinionFormData?: ReviewOpinionFormData | null,
|
||||
opnrNm?: string,
|
||||
): InstlSncnApplySaveRequestDto {
|
||||
return {
|
||||
...buildSavePayload(data, checklistFormState, reviewStep, prgrsSttsComCd),
|
||||
techReViewSaveCd,
|
||||
rvwStpCd: reviewStep,
|
||||
rvwOpnnList: toRvwOpnnSaveDto(data, reviewOpinionFormData, opnrNm),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildSupplementReqPayload(
|
||||
data: InstlSncnMngDetResponse,
|
||||
payload: SplmntDmndDialogConfirmPayload,
|
||||
checklistFormState: ChecklistFormState,
|
||||
): InstlSncnApplySaveRequestDto {
|
||||
const existingItems: InstlSncnSplmntDmndSaveDto[] = (data.splmntDmndList ?? []).map((item) =>
|
||||
toSplmntDmndSaveItem(item),
|
||||
);
|
||||
|
||||
const nextSn = existingItems.reduce((max, item) => Math.max(max, item.splmntDmndSn ?? 0), 0) + 1;
|
||||
|
||||
const newItems: InstlSncnSplmntDmndSaveDto[] = 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: InstlSncnSplmntDmndSaveDto): InstlSncnSplmntDmndSaveDto {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export interface ReviewerOption {
|
||||
uuid: string;
|
||||
name: string;
|
||||
deptNm: string;
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RvwMnpwMngResponseDto, useRvwMnpwMngPage } from "@/entities/rvw-mnpw";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Skeleton,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
import { type ReviewerOption } from "../../model/reviewer-option";
|
||||
|
||||
const PAGE_SIZE = 500;
|
||||
|
||||
interface AprvrPickerDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
atrzSeComCd: string;
|
||||
onSelect: (reviewer: ReviewerOption) => void;
|
||||
}
|
||||
|
||||
function AprvrPickerDialog({ open, onOpenChange, atrzSeComCd, onSelect }: AprvrPickerDialogProps) {
|
||||
const { data, isPending, isError } = useRvwMnpwMngPage(
|
||||
{
|
||||
atrzSeComCd,
|
||||
page: 0,
|
||||
size: PAGE_SIZE,
|
||||
sort: "userUuidNm,asc",
|
||||
},
|
||||
{ enabled: open },
|
||||
);
|
||||
|
||||
const items = data?.content ?? [];
|
||||
|
||||
function fromRvwMnpwMng(item: RvwMnpwMngResponseDto): ReviewerOption {
|
||||
return {
|
||||
uuid: item.userUuid,
|
||||
name: item.userUuidNm ?? "",
|
||||
deptNm: item.ognzNm ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
function handleSelect(item: (typeof items)[number]) {
|
||||
onSelect(fromRvwMnpwMng(item));
|
||||
onOpenChange(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[56rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>검토인력 선택</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="h-[36rem] overflow-auto">
|
||||
{isPending ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-[4rem]" />
|
||||
<Skeleton className="h-[16rem]" />
|
||||
</div>
|
||||
) : isError ? (
|
||||
<p className="py-8 text-center text-lg text-muted-foreground">
|
||||
검토인력 목록을 불러오지 못했습니다.
|
||||
</p>
|
||||
) : items.length === 0 ? (
|
||||
<p className="py-8 text-center text-lg text-muted-foreground">
|
||||
배치된 검토인력이 없습니다.
|
||||
</p>
|
||||
) : (
|
||||
<Table className="w-full">
|
||||
<colgroup>
|
||||
<col width="25%" />
|
||||
<col width="25%" />
|
||||
<col width="15%" />
|
||||
<col width="15%" />
|
||||
<col width="20%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>성명</TableHead>
|
||||
<TableHead>부서</TableHead>
|
||||
<TableHead>직위</TableHead>
|
||||
<TableHead>직급</TableHead>
|
||||
<TableHead>선택</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.map((item) => (
|
||||
<TableRow key={`${item.atrzSeComCd}-${item.ognzSn}-${item.userUuid}`}>
|
||||
<TableCell align="center">{item.userUuidNm || item.userUuid}</TableCell>
|
||||
<TableCell align="center">{item.ognzNm || "-"}</TableCell>
|
||||
<TableCell align="center">{item.jbpsSeNm || "-"}</TableCell>
|
||||
<TableCell align="center">{item.jbgdSeNm || "-"}</TableCell>
|
||||
<TableCell align="center">
|
||||
<Button type="button" size="sm" onClick={() => handleSelect(item)}>
|
||||
선택
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" color="white" onClick={() => onOpenChange(false)}>
|
||||
닫기
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { AprvrPickerDialog };
|
||||
@@ -1,163 +0,0 @@
|
||||
"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<ElaprTmplt | null>(null);
|
||||
const [aplnRows, setAplnRows] = useState<ElaprAplnRow[]>([]);
|
||||
const [isEditingLocked, setIsEditingLocked] = useState(false);
|
||||
|
||||
const tmpltQuery = useElaprTmpltPage({
|
||||
page: 0,
|
||||
size: PAGE_SIZE,
|
||||
sort: "regDt,desc",
|
||||
docSeComCd: "INSTL",
|
||||
});
|
||||
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="max-w-[120rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>결재선 선택</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex gap-6">
|
||||
<ElaprTmpltSelectTable
|
||||
query={tmpltQuery}
|
||||
selectedTmpltId={selectedTmplt?.elaprTmpltId ?? null}
|
||||
onSelectTmplt={handleSelectTmplt}
|
||||
/>
|
||||
<ElaprAplnPickerGrid
|
||||
elaprTmpltId={selectedTmplt?.elaprTmpltId ?? null}
|
||||
onRowsChange={handleRowsChange}
|
||||
onEditingLockedChange={handleEditingLockedChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" color="white" onClick={() => handleOpenChange(false)}>
|
||||
닫기
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={!selectedTmplt || isEditingLocked}
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
확인
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { ElaprTmpltPickerDialog };
|
||||
export type { ElaprTmpltPickerConfirmPayload };
|
||||
@@ -1,104 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import type { UseQueryResult } from "@tanstack/react-query";
|
||||
import { createColumnHelper, getCoreRowModel, type RowSelectionState } from "@tanstack/react-table";
|
||||
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import type { PageResponse } from "@/shared/model";
|
||||
import type { ElaprTmplt } from "@/entities/elapr";
|
||||
import { Skeleton } from "@common/components";
|
||||
|
||||
const columnHelper = createColumnHelper<ElaprTmplt>();
|
||||
|
||||
interface ElaprTmpltSelectTableProps {
|
||||
query: UseQueryResult<PageResponse<ElaprTmplt>>;
|
||||
selectedTmpltId: string | null;
|
||||
onSelectTmplt: (tmplt: ElaprTmplt | null) => void;
|
||||
}
|
||||
|
||||
function ElaprTmpltSelectTable({
|
||||
query,
|
||||
selectedTmpltId,
|
||||
onSelectTmplt,
|
||||
}: ElaprTmpltSelectTableProps) {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedTmpltId) {
|
||||
setRowSelection({ [selectedTmpltId]: true });
|
||||
} else {
|
||||
setRowSelection({});
|
||||
}
|
||||
}, [selectedTmpltId]);
|
||||
|
||||
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 ?? "—",
|
||||
}),
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: { rowSelection },
|
||||
enableRowSelection: true,
|
||||
enableMultiRowSelection: false,
|
||||
getRowId: (row) => row.elaprTmpltId,
|
||||
onRowSelectionChange: (updater) => {
|
||||
const next = typeof updater === "function" ? updater(rowSelection) : updater;
|
||||
setRowSelection(next);
|
||||
const selectedId = Object.keys(next)[0];
|
||||
if (!selectedId) {
|
||||
onSelectTmplt(null);
|
||||
return;
|
||||
}
|
||||
const selected = query.data?.content.find((t) => t.elaprTmpltId === selectedId);
|
||||
onSelectTmplt(selected ?? null);
|
||||
},
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
onRowClick: (row) => {
|
||||
setRowSelection({ [row.elaprTmpltId]: true });
|
||||
onSelectTmplt(row);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<section className="card card-border mx-0 min-h-[32rem] flex-[0.5] bg-background pt-0">
|
||||
<div className="card-header h-[4.6rem]">
|
||||
<h3 className="text-2xl font-semibold">전자결재서식</h3>
|
||||
</div>
|
||||
{query.isPending && (
|
||||
<div className="flex flex-col gap-2 p-4">
|
||||
<Skeleton className="h-[4rem]" />
|
||||
<Skeleton className="h-[16rem]" />
|
||||
</div>
|
||||
)}
|
||||
{query.isError && (
|
||||
<p className="py-8 text-center text-lg text-muted-foreground">
|
||||
결재서식 목록을 불러오지 못했습니다.
|
||||
</p>
|
||||
)}
|
||||
{!query.isPending && !query.isError && tableComponent}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export { ElaprTmpltSelectTable };
|
||||
@@ -11,8 +11,11 @@ import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import {
|
||||
buildElaprPrgrsSttsListFromAplnRows,
|
||||
ElaprApprovalDialog,
|
||||
type ElaprDmndSaveRequestDto,
|
||||
elaprDmndService,
|
||||
type ElaprTmpltPickerConfirmPayload,
|
||||
ElaprTmpltPickerDialog,
|
||||
hasCurrentUserPendingApprovalRequest,
|
||||
useElaprDocPrgrsByDocNo,
|
||||
} from "@/entities/elapr";
|
||||
@@ -23,8 +26,7 @@ import {
|
||||
type InstlSncnMngDetResponse,
|
||||
instlSncnService,
|
||||
} from "@/entities/instl-sncn";
|
||||
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
|
||||
import { TextInputDialog } from "@/widgets/pbadms-sprt";
|
||||
import { SplmntDmndDialog, type SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import {
|
||||
@@ -34,22 +36,23 @@ import {
|
||||
needsRoleUser01AtrzResolve,
|
||||
} from "../../lib/button-config";
|
||||
import { computeReviewStepChecklistProgress } from "../../lib/checklist-utils";
|
||||
import { buildSavePayload, buildTechReviewReplyPayload } from "../../lib/save-payload-utils";
|
||||
import {
|
||||
buildSavePayload,
|
||||
buildSupplementReqPayload,
|
||||
buildTechReviewReplyPayload,
|
||||
toSplmntDmndSaveItem,
|
||||
} from "../../lib/save-payload-utils";
|
||||
import { type ChecklistFormState } from "../../model/view-types";
|
||||
import type { TechReviewOpinionSectionHandle } from "../sections/tech-review-opinion-section";
|
||||
|
||||
import { ElaprApprovalDialog } from "./elapr-approval-dialog";
|
||||
import {
|
||||
type ElaprTmpltPickerConfirmPayload,
|
||||
ElaprTmpltPickerDialog,
|
||||
} from "./elapr-tmplt-picker-dialog";
|
||||
|
||||
interface FloatingActionButtonsProps {
|
||||
data: InstlSncnMngDetResponse;
|
||||
docSeComCd: string;
|
||||
instlSncnDocNo: string;
|
||||
prgrsSttsComCd: string;
|
||||
dtlInstCd: string | undefined;
|
||||
atrzSeComCdList: string[];
|
||||
isRvwLoginUserLoading: boolean;
|
||||
checklistFormState: ChecklistFormState;
|
||||
techReviewOpinionRef?: React.RefObject<TechReviewOpinionSectionHandle | null>;
|
||||
opnrNm?: string;
|
||||
@@ -63,6 +66,8 @@ function FloatingActionButtons({
|
||||
instlSncnDocNo,
|
||||
prgrsSttsComCd,
|
||||
dtlInstCd,
|
||||
atrzSeComCdList,
|
||||
isRvwLoginUserLoading,
|
||||
checklistFormState,
|
||||
techReviewOpinionRef,
|
||||
opnrNm,
|
||||
@@ -80,18 +85,9 @@ function FloatingActionButtons({
|
||||
|
||||
const needsAtrzResolve = needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd);
|
||||
const needsTeApprovalCheck = needsAtrzResolve && prgrsSttsComCd === "TE";
|
||||
const { data: rvwLoginUserData, isLoading: isRvwLoginUserLoading } = useRvwMnpwMngLoginUserList(
|
||||
{},
|
||||
{ enabled: needsAtrzResolve },
|
||||
);
|
||||
const { data: elaprDocPrgrsList = [], isLoading: isElaprDocPrgrsLoading } =
|
||||
useElaprDocPrgrsByDocNo(needsTeApprovalCheck ? docNo : null);
|
||||
|
||||
const atrzSeComCdList = useMemo(
|
||||
() => rvwLoginUserData?.content?.map((item) => item.atrzSeComCd) ?? [],
|
||||
[rvwLoginUserData],
|
||||
);
|
||||
|
||||
const hasPendingApprovalRequest = hasCurrentUserPendingApprovalRequest(
|
||||
elaprDocPrgrsList,
|
||||
userInfo?.userUnqId,
|
||||
@@ -382,15 +378,18 @@ function FloatingActionButtons({
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = buildTechReviewReplyPayload(
|
||||
data,
|
||||
checklistFormState,
|
||||
"KECO",
|
||||
"TE",
|
||||
"SV",
|
||||
getReviewOpinionFormData(),
|
||||
opnrNm,
|
||||
);
|
||||
const payload = {
|
||||
...buildTechReviewReplyPayload(
|
||||
data,
|
||||
checklistFormState,
|
||||
"KECO",
|
||||
"TE",
|
||||
"SV",
|
||||
getReviewOpinionFormData(),
|
||||
opnrNm,
|
||||
),
|
||||
splmntDmndList: (data.splmntDmndList ?? []).map(toSplmntDmndSaveItem),
|
||||
};
|
||||
|
||||
await executeAction({
|
||||
action: INSTL_SNCN_VIEW_BUTTON_ACTION.rvwCmptn,
|
||||
@@ -404,11 +403,12 @@ function FloatingActionButtons({
|
||||
setActiveDialog("dmndSplmnt");
|
||||
}
|
||||
|
||||
async function handleDmndSplmntConfirm(splmntCn: string) {
|
||||
async function handleDmndSplmntConfirm(payload: SplmntDmndDialogConfirmPayload) {
|
||||
setActiveDialog(null);
|
||||
const requestPayload = buildSupplementReqPayload(data, payload, checklistFormState);
|
||||
await executeAction({
|
||||
action: INSTL_SNCN_VIEW_BUTTON_ACTION.dmndSplmnt,
|
||||
request: splmntCn,
|
||||
request: requestPayload,
|
||||
successMessage: "보완요청이 처리되었습니다.",
|
||||
});
|
||||
}
|
||||
@@ -440,16 +440,6 @@ function FloatingActionButtons({
|
||||
삭제
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.splmntCmptn) && (
|
||||
<Button
|
||||
onClick={() =>
|
||||
void executeAction({ action: INSTL_SNCN_VIEW_BUTTON_ACTION.splmntCmptn })
|
||||
}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
보완완료
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.instlSncnAncmnt) && (
|
||||
<Button onClick={handleInstlSncnAncmntClick} disabled={isProcessing}>
|
||||
설치인가 고시
|
||||
@@ -516,16 +506,15 @@ function FloatingActionButtons({
|
||||
<ElaprTmpltPickerDialog
|
||||
open={activeDialog === "elaprTmplt"}
|
||||
onClose={() => setActiveDialog(null)}
|
||||
docSeComCd={docSeComCd}
|
||||
onConfirm={handleElaprTmpltConfirm}
|
||||
/>
|
||||
|
||||
<TextInputDialog
|
||||
<SplmntDmndDialog
|
||||
open={activeDialog === "dmndSplmnt"}
|
||||
onOpenChange={() => setActiveDialog(null)}
|
||||
title="보완요청 내용 입력"
|
||||
placeholder="보완요청 내용을 입력하세요."
|
||||
isSubmitting={isProcessing}
|
||||
onConfirm={(value) => void handleDmndSplmntConfirm(value)}
|
||||
onConfirm={handleDmndSplmntConfirm}
|
||||
/>
|
||||
|
||||
<ElaprApprovalDialog
|
||||
@@ -565,11 +554,6 @@ async function handleDel(context: ActionHandlerContext): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSplmntCmptn(context: ActionHandlerContext): Promise<void> {
|
||||
// TODO: 보완완료 API 호출
|
||||
context.onSuccess?.();
|
||||
}
|
||||
|
||||
async function handleChangePrgrsSttsComCd(
|
||||
context: ActionHandlerContext<DocPrgrsUpdateRequestDto>,
|
||||
): Promise<void> {
|
||||
@@ -603,9 +587,17 @@ async function handleDsgntRvwr(
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDmndSplmnt(context: ActionHandlerContext): Promise<void> {
|
||||
// TODO: 보완요청 API 호출
|
||||
context.onSuccess?.();
|
||||
async function handleDmndSplmnt(
|
||||
context: ActionHandlerContext<InstlSncnApplySaveRequestDto>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const docNo = context.data.detDto.instlSncnDocNo;
|
||||
if (!docNo) throw new Error("문서번호가 없습니다.");
|
||||
await instlSncnService.submitReqSupplement(docNo, context.request);
|
||||
context.onSuccess?.();
|
||||
} catch (error) {
|
||||
context.onError?.(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAtrzAprv(context: ActionHandlerContext): Promise<void> {
|
||||
@@ -618,16 +610,10 @@ async function handleAtrzRply(context: ActionHandlerContext): Promise<void> {
|
||||
context.onSuccess?.();
|
||||
}
|
||||
|
||||
async function handleGrndsChck(context: ActionHandlerContext): Promise<void> {
|
||||
// TODO: 현장점검 등록 API 호출
|
||||
context.onSuccess?.();
|
||||
}
|
||||
|
||||
const buttonActionHandlers: Record<InstlSncnViewButtonAction, ActionHandler> = {
|
||||
strg: handleSaveApply,
|
||||
aply: handleSaveApply,
|
||||
del: handleDel,
|
||||
splmntCmptn: handleSplmntCmptn,
|
||||
instlSncnAncmnt: handleChangePrgrsSttsComCd,
|
||||
envoSbmsn: handleChangePrgrsSttsComCd,
|
||||
rjct: handleChangePrgrsSttsComCd,
|
||||
@@ -640,7 +626,6 @@ const buttonActionHandlers: Record<InstlSncnViewButtonAction, ActionHandler> = {
|
||||
dmndSplmnt: handleDmndSplmnt,
|
||||
atrzAprv: handleAtrzAprv,
|
||||
atrzRply: handleAtrzRply,
|
||||
grndsChck: handleGrndsChck,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
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 { getApproverStatus } from "../../lib/approval-utils";
|
||||
|
||||
interface SortablePrgrsSttsRowProps {
|
||||
row: ElaprPrgrsSttsRow;
|
||||
displaySeqNo: number;
|
||||
isEditing: boolean;
|
||||
isLocked: boolean;
|
||||
dragDisabled: boolean;
|
||||
onStartEdit: () => void;
|
||||
onSave: () => void;
|
||||
onCancel: () => void;
|
||||
onDelete: () => void;
|
||||
onChange: (patch: Partial<ElaprPrgrsSttsRow>) => void;
|
||||
onPickAprvr: () => void;
|
||||
}
|
||||
|
||||
function SortablePrgrsSttsRow({
|
||||
row,
|
||||
displaySeqNo,
|
||||
isEditing,
|
||||
isLocked,
|
||||
dragDisabled,
|
||||
onStartEdit,
|
||||
onSave,
|
||||
onCancel,
|
||||
onDelete,
|
||||
onChange,
|
||||
onPickAprvr,
|
||||
}: SortablePrgrsSttsRowProps) {
|
||||
const { data: atrzSeCodes } = useCodeList("ATRZSE");
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: row.rowKey,
|
||||
disabled: dragDisabled,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.6 : 1,
|
||||
};
|
||||
|
||||
const isEditable = isPrgrsSttsRowEditable(row);
|
||||
const rowDisabled = isLocked && !isEditing;
|
||||
const atrzSeLabel = row.atrzSeComNm || findCodeLabel(atrzSeCodes, row.atrzSeComCd);
|
||||
const status = getApproverStatus(row.atrzPrgrsSttsComCd);
|
||||
const isRejectedRow = row.atrzPrgrsSttsComCd === "ATRZ_PRGRS_004";
|
||||
const rjctRsn = row.rjctRsn?.trim();
|
||||
|
||||
const statusBadge = (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center rounded-sm px-3 py-1.5 text-lg font-semibold",
|
||||
status.badgeClass,
|
||||
isRejectedRow && rjctRsn && "cursor-help underline decoration-dotted underline-offset-4",
|
||||
)}
|
||||
>
|
||||
{status.label}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<TableRow ref={setNodeRef} style={style} className={isEditing ? "bg-accent/30" : undefined}>
|
||||
<TableCell align="center" className="w-10">
|
||||
{!dragDisabled && (
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
className="cursor-grab !bg-transparent text-muted-foreground disabled:cursor-not-allowed disabled:opacity-40"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
aria-label="순서 변경"
|
||||
>
|
||||
<MoveVertical className="!size-6" />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center">{displaySeqNo}</TableCell>
|
||||
<TableCell align="center">
|
||||
{isEditing ? (
|
||||
<CodeSelect
|
||||
upCd="ATRZSE"
|
||||
className="min-w-[120px]"
|
||||
value={row.atrzSeComCd ?? ""}
|
||||
onValueChange={(v) =>
|
||||
onChange({
|
||||
atrzSeComCd: v,
|
||||
atrzSeComNm: findCodeLabel(atrzSeCodes, v),
|
||||
})
|
||||
}
|
||||
placeholder="결재구분"
|
||||
/>
|
||||
) : (
|
||||
atrzSeLabel || "-"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<span>{row.aprvrNm || row.aprvrUuid || "-"}</span>
|
||||
{isEditing && (
|
||||
<Button type="button" size="sm" onClick={onPickAprvr}>
|
||||
선택
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{!isEditing && (row.ognzNm || row.jbpsSeNm || row.jbgdSeNm) && (
|
||||
<div className="text-base text-gray-500">
|
||||
{row.ognzNm ?? ""}
|
||||
{row.jbpsSeNm ? ` · ${row.jbpsSeNm}` : ""}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{isRejectedRow && rjctRsn ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{statusBadge}</TooltipTrigger>
|
||||
<TooltipContent sideOffset={4} className="max-w-md whitespace-pre-wrap">
|
||||
{rjctRsn}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
statusBadge
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<div className="flex justify-center gap-2">
|
||||
{isEditing ? (
|
||||
<>
|
||||
<Button type="button" size="sm" onClick={onSave}>
|
||||
확인
|
||||
</Button>
|
||||
<Button type="button" size="sm" color="white" onClick={onCancel}>
|
||||
취소
|
||||
</Button>
|
||||
</>
|
||||
) : isEditable ? (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="white"
|
||||
disabled={rowDisabled}
|
||||
onClick={onStartEdit}
|
||||
>
|
||||
수정
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="gray"
|
||||
disabled={rowDisabled}
|
||||
onClick={onDelete}
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-sm text-muted-foreground">-</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export { SortablePrgrsSttsRow };
|
||||
@@ -10,6 +10,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import {
|
||||
type InstlSncnApplySaveRequestDto,
|
||||
instlSncnKeys,
|
||||
type InstlSncnMngDetResponse,
|
||||
instlSncnService,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
ContactInfoFormSection,
|
||||
isFormChecklistComplete,
|
||||
PrivacyConsentSection,
|
||||
SplmntDmndFormSection,
|
||||
} from "@/widgets/pbadms-sprt";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
@@ -48,6 +50,7 @@ function InstlFormPage({ id, mode, initialData, taskSeComCd }: InstlFormPageProp
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const isEditMode = mode === "edit";
|
||||
const isSplmntPrgrsStts = initialData?.detDto.prgrsSttsComCd === "SL";
|
||||
|
||||
const form = useForm<InstlSncnRegistFormData>({
|
||||
resolver: zodResolver(instlSncnRegistSchema),
|
||||
@@ -58,13 +61,13 @@ function InstlFormPage({ id, mode, initialData, taskSeComCd }: InstlFormPageProp
|
||||
: getCreateDefaultValues(userInfo, taskSeComCd),
|
||||
});
|
||||
|
||||
async function submitForm(
|
||||
function buildFormPayload(
|
||||
data: InstlSncnRegistFormData,
|
||||
prgrsSttsComCd: "SV" | "ST",
|
||||
): Promise<string | undefined> {
|
||||
prgrsSttsComCd: "SV" | "ST" | "TE",
|
||||
): InstlSncnApplySaveRequestDto {
|
||||
const { taskSeComCd: formTaskSeComCd, ...instlSncnMngRest } = data.instlSncnMng;
|
||||
|
||||
const payload = {
|
||||
return {
|
||||
instlSncnMng: {
|
||||
...instlSncnMngRest,
|
||||
instlSncnDocNo: isEditMode ? initialData?.detDto.instlSncnDocNo : undefined,
|
||||
@@ -78,9 +81,61 @@ function InstlFormPage({ id, mode, initialData, taskSeComCd }: InstlFormPageProp
|
||||
selected: item.selected,
|
||||
})),
|
||||
esntlAtchFileList: data.esntlAtchFileList,
|
||||
splmntDmndList: data.splmntDmndList.map(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
({ splmntAtchFileNm, splmntAtchFileAtflId, splmntDmndAtchFileNm, ...item }) => ({
|
||||
...item,
|
||||
...(prgrsSttsComCd === "TE" ? { cmptnYn: "Y" } : null),
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return instlSncnService.saveApply(payload);
|
||||
async function submitForm(
|
||||
data: InstlSncnRegistFormData,
|
||||
prgrsSttsComCd: "SV" | "ST",
|
||||
): Promise<string | undefined> {
|
||||
return instlSncnService.saveApply(buildFormPayload(data, prgrsSttsComCd));
|
||||
}
|
||||
|
||||
async function submitSupplementForm(data: InstlSncnRegistFormData): Promise<string | undefined> {
|
||||
const docNo = initialData?.detDto.instlSncnDocNo;
|
||||
if (!docNo) return undefined;
|
||||
return instlSncnService.submitSupplement(docNo, buildFormPayload(data, "TE"));
|
||||
}
|
||||
|
||||
async function onSplmntCmptn(data: InstlSncnRegistFormData) {
|
||||
if (isSubmitting) return;
|
||||
|
||||
const hasEmptySplmntCn = data.splmntDmndList?.some(
|
||||
(item) => !item.splmntCn || !item.splmntCn.trim(),
|
||||
);
|
||||
if (hasEmptySplmntCn) {
|
||||
alert("보완요청내용이 있습니다. 보완완료내용을 입력해 주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const docNo = initialData?.detDto.instlSncnDocNo;
|
||||
if (!docNo) {
|
||||
alert("문서번호가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const result = await submitSupplementForm(data);
|
||||
if (!result) return;
|
||||
|
||||
alert("보완완료 처리되었습니다.");
|
||||
router.push(`/instl-sncn/${docNo}`);
|
||||
queryClient.invalidateQueries({ queryKey: instlSncnKeys.list() });
|
||||
queryClient.invalidateQueries({ queryKey: instlSncnKeys.detail(docNo) });
|
||||
} catch (error) {
|
||||
console.error("보완완료 실패:", error);
|
||||
alert("보완완료 처리 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onSave(data: InstlSncnRegistFormData) {
|
||||
@@ -131,10 +186,6 @@ function InstlFormPage({ id, mode, initialData, taskSeComCd }: InstlFormPageProp
|
||||
}
|
||||
}
|
||||
|
||||
function handleApplyClick() {
|
||||
form.handleSubmit(onApply)();
|
||||
}
|
||||
|
||||
async function handleDelClick() {
|
||||
if (isSubmitting) return;
|
||||
|
||||
@@ -150,6 +201,7 @@ function InstlFormPage({ id, mode, initialData, taskSeComCd }: InstlFormPageProp
|
||||
setIsSubmitting(true);
|
||||
await instlSncnService.remove(docNo);
|
||||
alert("삭제되었습니다.");
|
||||
queryClient.invalidateQueries({ queryKey: instlSncnKeys.list() });
|
||||
router.push("/instl-sncn");
|
||||
} catch (error) {
|
||||
console.error("삭제 실패:", error);
|
||||
@@ -174,6 +226,7 @@ function InstlFormPage({ id, mode, initialData, taskSeComCd }: InstlFormPageProp
|
||||
initialEsntlFiles={isEditMode ? initialData?.esntlFileList : undefined}
|
||||
initialEtcFiles={isEditMode ? initialData?.etcFileList : undefined}
|
||||
/>
|
||||
<SplmntDmndFormSection readOnly={!isSplmntPrgrsStts} />
|
||||
|
||||
<div className="sticky bottom-0 z-40 !-mb-2 border-t bg-white pb-8 pt-6 shadow-[0_-4px_12px_rgba(0,0,0,0.06)]">
|
||||
<div className="mx-4 flex flex-wrap justify-center gap-3 sm:gap-4">
|
||||
@@ -185,23 +238,40 @@ function InstlFormPage({ id, mode, initialData, taskSeComCd }: InstlFormPageProp
|
||||
>
|
||||
목록
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
저장
|
||||
</Button>
|
||||
{isEditMode && (
|
||||
{!isSplmntPrgrsStts ? (
|
||||
<>
|
||||
<Button
|
||||
color="green"
|
||||
type="button"
|
||||
onClick={handleApplyClick}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
신청완료
|
||||
</Button>
|
||||
<Button color="red" type="button" onClick={handleDelClick} disabled={isSubmitting}>
|
||||
삭제
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
저장
|
||||
</Button>
|
||||
{isEditMode && (
|
||||
<>
|
||||
<Button
|
||||
color="green"
|
||||
type="button"
|
||||
onClick={form.handleSubmit(onApply)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
신청완료
|
||||
</Button>
|
||||
<Button
|
||||
color="red"
|
||||
type="button"
|
||||
onClick={handleDelClick}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={form.handleSubmit(onSplmntCmptn)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
보완완료
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import { useMemo, useRef } from "react";
|
||||
|
||||
import { useUserStore } from "@/shared/lib";
|
||||
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
|
||||
import { useInstlSncnDetail } from "@/entities/instl-sncn";
|
||||
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
|
||||
import { SectionCollapsible, SupplementDemandSection } from "@/widgets/pbadms-sprt";
|
||||
import { Skeleton } from "@common/components";
|
||||
|
||||
import { needsRoleUser01AtrzResolve } from "../lib/button-config";
|
||||
import { getEditableReviewStep } from "../lib/review-step-utils";
|
||||
import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-checklist-form-state";
|
||||
|
||||
@@ -32,6 +36,17 @@ function InstlViewPage({ id }: InstlViewPageProps) {
|
||||
const { formState: checklistFormState, setFormState: setChecklistFormState } =
|
||||
useChecklistFormState(data?.cklstResultList ?? EMPTY_CKLST_RESULT_LIST);
|
||||
|
||||
const prgrsSttsComCd = data?.detDto?.prgrsSttsComCd || "";
|
||||
const needsAtrzResolve = needsRoleUser01AtrzResolve(userInfo?.dtlInstCd, prgrsSttsComCd);
|
||||
const { data: rvwLoginUserData, isLoading: isRvwLoginUserLoading } = useRvwMnpwMngLoginUserList(
|
||||
{},
|
||||
{ enabled: needsAtrzResolve },
|
||||
);
|
||||
const atrzSeComCdList = useMemo(
|
||||
() => rvwLoginUserData?.content?.map((item) => item.atrzSeComCd) ?? [],
|
||||
[rvwLoginUserData],
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="mt-6 flex flex-col gap-4 px-6">
|
||||
@@ -63,6 +78,7 @@ function InstlViewPage({ id }: InstlViewPageProps) {
|
||||
esntlFileList,
|
||||
etcFileList,
|
||||
rvwList = [],
|
||||
splmntDmndList = [],
|
||||
} = data;
|
||||
|
||||
const editableReviewStep = getEditableReviewStep(
|
||||
@@ -74,6 +90,9 @@ function InstlViewPage({ id }: InstlViewPageProps) {
|
||||
<div className="mt-6 space-y-4">
|
||||
<BasicInfoSection det={detDto} />
|
||||
<ContactInfoSection det={detDto} />
|
||||
<SectionCollapsible title="결재현황">
|
||||
<ElaprDocPrgrsStatus elaprDocNo={detDto.instlSncnDocNo} />
|
||||
</SectionCollapsible>
|
||||
<BusinessScaleSection bizSclList={bizSclList} />
|
||||
<BusinessCostSection bizCstList={bizCstList} />
|
||||
<ChecklistSection
|
||||
@@ -88,13 +107,18 @@ function InstlViewPage({ id }: InstlViewPageProps) {
|
||||
ref={techReviewOpinionRef}
|
||||
rvwList={rvwList}
|
||||
editableReviewStep={editableReviewStep}
|
||||
atrzSeComCdList={atrzSeComCdList}
|
||||
isAtrzLoading={needsAtrzResolve && isRvwLoginUserLoading}
|
||||
/>
|
||||
<SupplementDemandSection splmntDmndList={splmntDmndList} />
|
||||
<FloatingActionButtons
|
||||
data={data}
|
||||
docSeComCd="INSTL"
|
||||
instlSncnDocNo={detDto.instlSncnDocNo}
|
||||
prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
|
||||
dtlInstCd={userInfo?.dtlInstCd}
|
||||
atrzSeComCdList={atrzSeComCdList}
|
||||
isRvwLoginUserLoading={needsAtrzResolve && isRvwLoginUserLoading}
|
||||
checklistFormState={checklistFormState}
|
||||
techReviewOpinionRef={techReviewOpinionRef}
|
||||
opnrNm={userInfo?.userName}
|
||||
|
||||
@@ -12,6 +12,12 @@ function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>사업명</TableHead>
|
||||
|
||||
@@ -29,6 +29,8 @@ const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
||||
interface TechReviewOpinionSectionProps {
|
||||
rvwList: InstlSncnRvwOpnnResponseDto[];
|
||||
editableReviewStep?: ReviewStepCode;
|
||||
atrzSeComCdList?: string[];
|
||||
isAtrzLoading?: boolean;
|
||||
}
|
||||
|
||||
export interface TechReviewOpinionSectionHandle {
|
||||
@@ -65,7 +67,13 @@ function hasFormContent(formData: ReviewOpinionFormData) {
|
||||
const TechReviewOpinionSection = forwardRef<
|
||||
TechReviewOpinionSectionHandle,
|
||||
TechReviewOpinionSectionProps
|
||||
>(function TechReviewOpinionSection({ rvwList, editableReviewStep }, ref) {
|
||||
>(function TechReviewOpinionSection({
|
||||
rvwList,
|
||||
editableReviewStep,
|
||||
atrzSeComCdList = [],
|
||||
isAtrzLoading = false,
|
||||
}, ref) {
|
||||
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||
...EMPTY_FORM_DATA,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
@@ -98,6 +106,9 @@ const TechReviewOpinionSection = forwardRef<
|
||||
if (!editableReviewStep || !hasFormContent(formData)) {
|
||||
return null;
|
||||
}
|
||||
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...formData,
|
||||
@@ -112,14 +123,15 @@ const TechReviewOpinionSection = forwardRef<
|
||||
setUploadedFile(null);
|
||||
},
|
||||
}),
|
||||
[editableReviewStep, formData],
|
||||
[canWriteKecoOpinion, editableReviewStep, formData],
|
||||
);
|
||||
|
||||
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
|
||||
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
||||
|
||||
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
|
||||
const hasKecoSection = kecoRvwList.length > 0 || editableReviewStep === "KECO";
|
||||
const hasKecoSection =
|
||||
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteKecoOpinion);
|
||||
|
||||
if (!hasEnvSection && !hasKecoSection) {
|
||||
return null;
|
||||
@@ -163,12 +175,13 @@ const TechReviewOpinionSection = forwardRef<
|
||||
|
||||
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
||||
if (editableReviewStep !== reviewStep) return null;
|
||||
if (reviewStep === "KECO" && !canWriteKecoOpinion) return null;
|
||||
|
||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||
|
||||
return (
|
||||
<div className="p-2">
|
||||
<h4 className="mb-4 text-xl font-semibold">검토의견 작성</h4>
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -267,14 +280,18 @@ const TechReviewOpinionSection = forwardRef<
|
||||
<>
|
||||
{hasEnvSection ? (
|
||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
|
||||
{envRvwList.length > 0 ? <TechReviewOpinionTable items={envRvwList} /> : null}
|
||||
{renderOpinionForm("ENV")}
|
||||
<div className="space-y-6">
|
||||
{envRvwList.length > 0 ? <TechReviewOpinionTable items={envRvwList} /> : null}
|
||||
{renderOpinionForm("ENV")}
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
) : null}
|
||||
{hasKecoSection ? (
|
||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
||||
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
|
||||
{renderOpinionForm("KECO")}
|
||||
<div className="space-y-6">
|
||||
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
|
||||
{renderOpinionForm("KECO")}
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@@ -17,6 +17,11 @@ export const ENDPOINT = {
|
||||
ADSTDSTR: "/as-service/api/v1/adstdstr",
|
||||
FNRS_CNSLTN_MNG: "/as-service/api/v1/fnrs-cnsltn-mng",
|
||||
FNRS_SHAR_SE: "/as-service/api/v1/fnrs-shar-se-mng",
|
||||
DESIGN_CNSTN: "/as-service/api/v1/design-cnstn",
|
||||
DESIGN_CNSTN_BIZ_CST: "/as-service/api/v1/design-cnstn-biz-cst",
|
||||
DESIGN_CNSTN_BIZ_SCL: "/as-service/api/v1/design-cnstn-biz-scl",
|
||||
DESIGN_CNSTN_ESNTL_ATCH_FILE: "/as-service/api/v1/design-cnstn-esntl-atch-file",
|
||||
DESIGN_CNSTN_RVW_OPNN: "/as-service/api/v1/design-cnstn-rvw-opnn",
|
||||
},
|
||||
USER_SERVICE: {
|
||||
LOGIN: "/user-service/login",
|
||||
|
||||
@@ -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;
|
||||
@@ -151,6 +152,7 @@ export interface ElaprDocPrgrsClientResponseDto {
|
||||
jbpsSeNm?: string;
|
||||
jbgdSeNm?: string;
|
||||
ognzNm?: string;
|
||||
dcarbYn?: string;
|
||||
}
|
||||
|
||||
/** 사업규모 저장 공통 필드 */
|
||||
|
||||
@@ -30,5 +30,11 @@ export { ContactInfoFormSection } from "./ui/contact-info-form-section";
|
||||
export { FnrsSharFormSection } from "./ui/fnrs-shar-form-section";
|
||||
export { PrivacyConsentSection } from "./ui/privacy-consent-section";
|
||||
export { SectionCollapsible } from "./ui/section-collapsible";
|
||||
export {
|
||||
SplmntDmndDialog,
|
||||
type SplmntDmndDialogConfirmItem,
|
||||
type SplmntDmndDialogConfirmPayload,
|
||||
} from "./ui/splmnt-dmnd-dialog";
|
||||
export { type SplmntDmndFormItem, SplmntDmndFormSection } from "./ui/splmnt-dmnd-form-section";
|
||||
export { SupplementDemandSection } from "./ui/supplement-demand-section";
|
||||
export { TaskSePickerDialog } from "./ui/task-se-picker-dialog";
|
||||
export { TextInputDialog } from "./ui/text-input-dialog";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getDefaultFileKey } from "@/shared/lib";
|
||||
|
||||
export type FieldPrefix = "fnrsCnsltnMng" | "instlSncnMng";
|
||||
export type FieldPrefix = "fnrsCnsltnMng" | "instlSncnMng" | "designCnstnMng";
|
||||
|
||||
export const ETC_FILE_KEY_PREFIX = "etc::";
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user