Merge branch 'main' into deploy

This commit is contained in:
2026-08-06 16:22:09 +09:00
26 changed files with 275 additions and 12 deletions
+45
View File
@@ -0,0 +1,45 @@
import type { Metadata } from "next";
import Image from "next/image";
import "@/app/styles/globals.css";
export const metadata: Metadata = {
title: "행정지원시스템",
description: "페이지가 존재하지 않습니다. 관리자에게 문의하여 주십시오.",
};
export default function GlobalNotFound() {
return (
<html lang="ko">
<body>
<main className="flex min-h-screen flex-col items-center justify-center bg-[#f0f4f8] px-4 py-16">
<div className="mb-16 text-center">
<h1 className="text-[3.2rem] font-bold leading-snug tracking-tight">
<span className="text-primary">행정지원시스템</span>에 오신 것을
환영합니다.
</h1>
<p className="mt-6 text-[2rem] text-[#666]">
오류가 발생하였습니다.
</p>
</div>
<section className="card card-border flex w-full max-w-[72rem] flex-col items-center !rounded-2xl !py-16">
<Image
src="/images/magnifying-glass-with-warning.png"
alt=""
width={280}
height={280}
className="size-[28rem] object-contain"
/>
<p className="mt-4 text-[2.4rem] font-bold text-[#444]">
페이지가 존재하지 않습니다.
</p>
<p className="mt-2 text-[1.6rem] text-[#666]">
관리자에게 문의하여 주십시오
</p>
</section>
</main>
</body>
</html>
);
}
+3
View File
@@ -4,6 +4,9 @@ const nextConfig: NextConfig = {
output: "standalone",
reactStrictMode: true,
transpilePackages: ["@common/assets"],
experimental: {
globalNotFound: true,
},
};
export default nextConfig;
Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

@@ -16,7 +16,7 @@ export function validateAtrzOrder(rows: AtrzOrderValidateRow[]): string | null {
const visibleRows = rows.filter((row) => !row.isDeleted);
const finalCount = visibleRows.filter((row) => row.atrzSeComCd === ATRZ_FINAL_CD).length;
if (finalCount === 0) {
if (visibleRows.length > 0 && finalCount === 0) {
return "최종승인자는 반드시 등록해야 합니다.";
}
if (finalCount > 1) {
@@ -52,7 +52,8 @@ export function validateAllRowsForSave(
}
const visibleRows = getVisibleRows(rows);
if (visibleRows.length === 0) {
const hasDeletedDirtyRows = rows.some((row) => row.isDeleted && row.isDirty);
if (visibleRows.length === 0 && !hasDeletedDirtyRows) {
return "저장할 결재선이 없습니다.";
}
@@ -3,13 +3,15 @@
import { useMemo, useState } from "react";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useQueryClient } from "@tanstack/react-query";
import { getCoreRowModel, type RowSelectionState } from "@tanstack/react-table";
import { useDataTable } from "@/shared/lib";
import { ENDPOINT } from "@/shared/config";
import { useDataTable, useMutationWrapper } from "@/shared/lib";
import {
type ElaprTmplt,
elaprTmpltService,
useCreateElaprTmplt,
useDeleteElaprTmplt,
useElaprTmpltPage,
useUpdateElaprTmplt,
} from "@/entities/elapr";
@@ -26,6 +28,7 @@ interface ElaprTmpltListProps {
function ElaprTmpltList({ selectedTmpltId, onSelectTmplt }: ElaprTmpltListProps) {
const { confirm } = useConfirm();
const queryClient = useQueryClient();
const [dialogMode, setDialogMode] = useState<"create" | "edit" | null>(null);
const [editingTmplt, setEditingTmplt] = useState<ElaprTmplt | null>(null);
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
@@ -38,7 +41,14 @@ function ElaprTmpltList({ selectedTmpltId, onSelectTmplt }: ElaprTmpltListProps)
const createMutation = useCreateElaprTmplt();
const updateMutation = useUpdateElaprTmplt();
const deleteMutation = useDeleteElaprTmplt();
const { mutateAsync: deleteTmplt } = useMutationWrapper({
mutationFn: (elaprTmpltId: string) => elaprTmpltService.remove(elaprTmpltId),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT],
});
},
});
const columns = useMemo(
() =>
@@ -49,13 +59,13 @@ function ElaprTmpltList({ selectedTmpltId, onSelectTmplt }: ElaprTmpltListProps)
},
onDelete: async (row) => {
if (!(await confirm("선택한 결재서식을 삭제하시겠습니까?"))) return;
await deleteMutation.mutateAsync(row.elaprTmpltId);
await deleteTmplt(row.elaprTmpltId);
if (selectedTmpltId === row.elaprTmpltId) {
onSelectTmplt(null);
}
},
}),
[confirm, deleteMutation, onSelectTmplt, selectedTmpltId],
[confirm, deleteTmplt, onSelectTmplt, selectedTmpltId],
);
const { tableComponent } = useDataTable({
@@ -82,7 +82,13 @@ function SortableAplnRow({
<Select
value={row.atrzSeComCd ?? ""}
onValueChange={(v) =>
onChange({ atrzSeComCd: v, atrzSeComNm: findCodeLabel(selectableAtrzSeCodes, v) })
onChange({
atrzSeComCd: v,
atrzSeComNm: findCodeLabel(selectableAtrzSeCodes, v),
aprvrUserId: "",
aprvrUuid: "",
aprvrNm: "",
})
}
>
<SelectTrigger className="min-w-[120px]">
+1
View File
@@ -5,5 +5,6 @@ export { queryClient } from "./query-client";
export { useAlertStore } from "./use-alert-store";
export { useDataTable } from "./use-data-table";
export { useMenuStore } from "./use-menu-store";
export { useMutationWrapper } from "./use-mutation-wrapper";
export { getDefaultFileKey, useTus } from "./use-tus";
export { useUserStore } from "./use-user-store";
@@ -0,0 +1,29 @@
import { useMutation, type UseMutationOptions } from "@tanstack/react-query";
import { AxiosError } from "axios";
import { useAlertStore } from "./use-alert-store";
export const useMutationWrapper = <
TData = unknown,
TError = unknown,
TVariables = void,
TContext = unknown,
>(
mutationOptions: UseMutationOptions<TData, TError, TVariables, TContext>,
) => {
const alert = useAlertStore((state) => state.setMessage);
return useMutation({
...mutationOptions,
onError: (error) => {
let errorMessage = "";
if (error instanceof AxiosError) {
if (error.response?.data?.message) errorMessage += `${error.response.data.message}\n`;
if (error.response?.data?.errors[0]?.message)
errorMessage += `${error.response.data.errors[0].message}\n`;
}
alert(`${errorMessage}`);
},
});
};
+1
View File
@@ -5,6 +5,7 @@ import presetConfig from "../../tailwind.config.preset";
const config: Config = {
presets: [presetConfig],
content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./src/**/*.{js,ts,jsx,tsx,mdx}",
"../../packages/common/components/dist/**/*.{js,ts,jsx,tsx,mdx}",
],
+46
View File
@@ -0,0 +1,46 @@
import type { Metadata } from "next";
import Image from "next/image";
import "@/app/styles/base.css";
import "@/app/styles/components.css";
export const metadata: Metadata = {
title: "유역하수도 통합 정보플랫폼",
description: "페이지가 존재하지 않습니다. 관리자에게 문의하여 주십시오.",
};
export default function GlobalNotFound() {
return (
<html lang="ko">
<body>
<main className="flex min-h-screen flex-col items-center justify-center bg-[#f0f4f8] px-4 py-16">
<div className="mb-16 text-center">
<h1 className="text-[3.2rem] font-bold leading-snug tracking-tight">
<span className="text-primary">유역하수도 통합 정보플랫폼</span>에
오신 것을 환영합니다.
</h1>
<p className="mt-6 text-[2rem] text-[#666]">
오류가 발생하였습니다.
</p>
</div>
<section className="card card-border flex w-full max-w-[72rem] flex-col items-center !rounded-2xl !py-16">
<Image
src="/images/magnifying-glass-with-warning.png"
alt=""
width={280}
height={280}
className="size-[28rem] object-contain"
/>
<p className="mt-4 text-[2.4rem] font-bold text-[#444]">
페이지가 존재하지 않습니다.
</p>
<p className="mt-2 text-[1.6rem] text-[#666]">
관리자에게 문의하여 주십시오
</p>
</section>
</main>
</body>
</html>
);
}
+1
View File
@@ -7,6 +7,7 @@ const nextConfig: NextConfig = {
output: "standalone",
experimental: {
proxyTimeout: 600000,
globalNotFound: true,
},
eslint: {
ignoreDuringBuilds: true,
Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

+1
View File
@@ -5,6 +5,7 @@ import presetConfig from "../../tailwind.config.preset";
export default {
presets: [presetConfig],
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"./src/**/*.{js,ts,jsx,tsx}",
"../../packages/common/components/src/ui/**/*.{js,ts,jsx,tsx}",
],
+46
View File
@@ -0,0 +1,46 @@
import type { Metadata } from "next";
import Image from "next/image";
import "@/app/styles/base.css";
import "@/app/styles/components.css";
export const metadata: Metadata = {
title: "하수도플랫폼 통합관리시스템",
description: "페이지가 존재하지 않습니다. 관리자에게 문의하여 주십시오.",
};
export default function GlobalNotFound() {
return (
<html lang="ko">
<body>
<main className="flex min-h-screen flex-col items-center justify-center bg-[#f0f4f8] px-4 py-16">
<div className="mb-16 text-center">
<h1 className="text-[3.2rem] font-bold leading-snug tracking-tight">
<span className="text-primary">하수도플랫폼 통합관리시스템</span>
에 오신 것을 환영합니다.
</h1>
<p className="mt-6 text-[2rem] text-[#666]">
오류가 발생하였습니다.
</p>
</div>
<section className="card card-border flex w-full max-w-[72rem] flex-col items-center !rounded-2xl !py-16">
<Image
src="/images/magnifying-glass-with-warning.png"
alt=""
width={280}
height={280}
className="size-[28rem] object-contain"
/>
<p className="mt-4 text-[2.4rem] font-bold text-[#444]">
페이지가 존재하지 않습니다.
</p>
<p className="mt-2 text-[1.6rem] text-[#666]">
관리자에게 문의하여 주십시오
</p>
</section>
</main>
</body>
</html>
);
}
+1
View File
@@ -7,6 +7,7 @@ const nextConfig: NextConfig = {
output: "standalone",
experimental: {
proxyTimeout: 600000,
globalNotFound: true,
},
eslint: {
ignoreDuringBuilds: true,
Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

+2
View File
@@ -1,10 +1,12 @@
import type { Config } from "tailwindcss";
import tailwindcssAnimate from "tailwindcss-animate";
import presetConfig from "../../tailwind.config.preset";
export default {
presets: [presetConfig],
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"./src/**/*.{js,ts,jsx,tsx}",
"../../packages/common/components/src/ui/**/*.{js,ts,jsx,tsx}",
],
@@ -97,7 +97,7 @@ function WithBaseLayoutInner({ children }: { children: React.ReactNode }) {
useEffect(() => {
if (!getToken(CLAIM_NAME)) return;
void statisticsApi.refreshCommonCodes().catch(() => {
void statisticsApi.refreshAllCommonCodes().catch(() => {
// 캐시 동기화 실패는 화면 진입을 막지 않음
});
}, []);
+45
View File
@@ -0,0 +1,45 @@
import type { Metadata } from "next";
import Image from "next/image";
import "@/app/styles/base.css";
export const metadata: Metadata = {
title: "유역하수도 통합 정보플랫폼",
description: "페이지가 존재하지 않습니다. 관리자에게 문의하여 주십시오.",
};
export default function GlobalNotFound() {
return (
<html lang="ko">
<body>
<main className="flex min-h-screen flex-col items-center justify-center bg-[#f0f4f8] px-4 py-16">
<div className="mb-16 text-center">
<h1 className="text-[3.2rem] font-bold leading-snug tracking-tight">
<span className="text-primary">유역하수도 통합 정보플랫폼</span>에
오신 것을 환영합니다.
</h1>
<p className="mt-6 text-[2rem] text-[#666]">
오류가 발생하였습니다.
</p>
</div>
<section className="card card-border flex w-full max-w-[72rem] flex-col items-center !rounded-2xl !py-16">
<Image
src="/images/magnifying-glass-with-warning.png"
alt=""
width={280}
height={280}
className="size-[28rem] object-contain"
/>
<p className="mt-4 text-[2.4rem] font-bold text-[#444]">
페이지가 존재하지 않습니다.
</p>
<p className="mt-2 text-[1.6rem] text-[#666]">
관리자에게 문의하여 주십시오
</p>
</section>
</main>
</body>
</html>
);
}
+3
View File
@@ -5,6 +5,9 @@ const subDomain =
const nextConfig: NextConfig = {
output: "standalone",
experimental: {
globalNotFound: true,
},
async headers() {
return [
{
Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

@@ -279,7 +279,6 @@ const NotEnteredStatus = () => {
return;
}
const params = buildUnSubmitDataRequest(values, userInfo);
console.log(params);
if (!params.dtlInstCd) {
alert("기관 정보를 확인할 수 없습니다.");
return;
@@ -370,7 +370,7 @@ export class StatisticsApiService implements StatisticsService {
return response.data;
}
/** 공통 코드 메모리 캐시 동기화 */
/** 입력 서비스 공통 코드 메모리 캐시 동기화 */
public async refreshCommonCodes(): Promise<CodesRefreshResponse> {
const response = await stClient.post<CodesRefreshResponse>(
`${ENDPOINT.ST_SERVICE.INPUTCOMMON}/codes-refresh`,
@@ -379,6 +379,26 @@ export class StatisticsApiService implements StatisticsService {
);
return response.data;
}
/** 관리 서비스 공통 코드 메모리 캐시 동기화 */
public async refreshManagementCommonCodes(): Promise<CodesRefreshResponse> {
const response = await stClient.post<CodesRefreshResponse>(
`${ENDPOINT.ST_SERVICE.COMMON}/codes/refresh`,
null,
withAuthHeader(),
);
return response.data;
}
/** 입력/관리 공통 코드 캐시 동기화 (한쪽 실패해도 다른 쪽은 완료) */
public async refreshAllCommonCodes(): Promise<
PromiseSettledResult<CodesRefreshResponse>[]
> {
return Promise.allSettled([
this.refreshCommonCodes(),
this.refreshManagementCommonCodes(),
]);
}
}
export const statisticsApi = new StatisticsApiService();
@@ -572,4 +572,6 @@ export interface StatisticsService {
CheckStatusCode(param: CheckStatusCodeRequest): Promise<CheckStatusCodeResponse>;
getUnSubmitData(param: unSubmitDataRequest): Promise<unSubmitDataResponse>;
refreshCommonCodes(): Promise<CodesRefreshResponse>;
refreshManagementCommonCodes(): Promise<CodesRefreshResponse>;
refreshAllCommonCodes(): Promise<PromiseSettledResult<CodesRefreshResponse>[]>;
}
@@ -23,7 +23,7 @@ function LoginPage() {
try {
await authService.login({ userId, password });
try {
await statisticsApi.refreshCommonCodes();
await statisticsApi.refreshAllCommonCodes();
} catch {
// 로그인 성공 후 캐시 동기화 실패는 진입을 막지 않음
}
+1
View File
@@ -5,6 +5,7 @@ import presetConfig from "../../tailwind.config.preset";
export default {
presets: [presetConfig],
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"./src/**/*.{js,ts,jsx,tsx}",
"../../packages/common/components/src/ui/**/*.{js,ts,jsx,tsx}",
],