diff --git a/apps/dm/app/(with-layout)/contents/page.tsx b/apps/dm/app/(with-layout)/contents/page.tsx
new file mode 100644
index 000000000..27d0974be
--- /dev/null
+++ b/apps/dm/app/(with-layout)/contents/page.tsx
@@ -0,0 +1,9 @@
+export const dynamic = "force-dynamic";
+import { ContentsPage } from "@/components/ui/contents";
+
+export default async function Contents() {
+ const contsSn = 28;
+ const menuCd = "24";
+
+ return ;
+}
diff --git a/apps/dm/app/(with-layout)/example/page.tsx b/apps/dm/app/(with-layout)/example/page.tsx
index b32b3346e..94e908d81 100644
--- a/apps/dm/app/(with-layout)/example/page.tsx
+++ b/apps/dm/app/(with-layout)/example/page.tsx
@@ -1,3 +1,6 @@
+/**
+ * 물 재이용 사례
+ */
import SubPageHeader from "@/components/SubPageHeader";
import BusinessSubNav, { type BusinessSubNavMenu } from "@/components/BusinessSubNav";
diff --git a/apps/dm/app/(with-layout)/status/_components/SewageStatusSection.tsx b/apps/dm/app/(with-layout)/status/_components/SewageStatusSection.tsx
index cd660a929..f7eb49a84 100644
--- a/apps/dm/app/(with-layout)/status/_components/SewageStatusSection.tsx
+++ b/apps/dm/app/(with-layout)/status/_components/SewageStatusSection.tsx
@@ -287,7 +287,7 @@ export default function SewageStatusSection() {
const isSubItem = type === "red-item" || type === "blue-item";
if (index === 0) {
- console.log(header.id, value);
+ // console.log(header.id, value);
return (
{isSubItem ? (
diff --git a/apps/dm/app/(with-layout)/status/page.tsx b/apps/dm/app/(with-layout)/status/page.tsx
index b18662c06..8bed3d04f 100644
--- a/apps/dm/app/(with-layout)/status/page.tsx
+++ b/apps/dm/app/(with-layout)/status/page.tsx
@@ -58,7 +58,7 @@ const menus: BusinessSubNavMenu[] = [
},
{
id: "status-treated-usage",
- label: "연도별 하수처리수 재이용 현황",
+ label: "용도별 하수처리수 재이용 현황",
},
{
id: "status-treated-facility",
diff --git a/apps/dm/app/layout.tsx b/apps/dm/app/layout.tsx
index 1fd9ea1cc..49c6c357c 100644
--- a/apps/dm/app/layout.tsx
+++ b/apps/dm/app/layout.tsx
@@ -16,7 +16,7 @@ export default function RootLayout({
물재이용 대민
-
+
{children}
diff --git a/apps/dm/app/main/layout.tsx b/apps/dm/app/main/layout.tsx
index b1304a66a..873024784 100644
--- a/apps/dm/app/main/layout.tsx
+++ b/apps/dm/app/main/layout.tsx
@@ -1,12 +1,10 @@
// app/main/layout.tsx
import "../reuse.module.css";
-import TopNav from "@/components/TopNav";
import { StoreProvider } from "../store";
export default function ReuseLandingLayout({ children }: { children: React.ReactNode }) {
return (
-
{children} {/* TopNav 높이만큼 */}
);
diff --git a/apps/dm/app/store/store.tsx b/apps/dm/app/store/store.tsx
index c1d4fcdfd..3b7b2c24d 100644
--- a/apps/dm/app/store/store.tsx
+++ b/apps/dm/app/store/store.tsx
@@ -33,7 +33,7 @@ export const StoreZus = (
return createStore ()((set) => {
if (typeof window !== "undefined") {
tokenManager.setToken("site-id-key", process.env.NEXT_PUBLIC_SITE_ID||"DM");
- tokenManager.setToken("site-theme-key", "green");
+ tokenManager.setToken("site-theme-key", "white");
}
return {
diff --git a/apps/dm/components/BusinessSubNav.tsx b/apps/dm/components/BusinessSubNav.tsx
index 6b4246412..d8753b5dd 100644
--- a/apps/dm/components/BusinessSubNav.tsx
+++ b/apps/dm/components/BusinessSubNav.tsx
@@ -1,3 +1,6 @@
+/**
+ * 우측 메뉴
+ */
"use client";
import Link from "next/link";
@@ -47,44 +50,67 @@ export default function BusinessSubNav({
setOpenMenus(initialOpenState);
}, [menus]);
- useEffect(() => {
- if (!allSections.length) return;
+useEffect(() => {
+ if (!allSections.length) return;
- const observer = new IntersectionObserver(
- (entries) => {
- const visible = entries
- .filter((entry) => entry.isIntersecting)
- .sort((a, b) => b.intersectionRatio - a.intersectionRatio);
+ const targets = allSections
+ .map((section) => document.getElementById(section.id))
+ .filter(Boolean) as HTMLElement[];
- if (visible[0]) {
- setActiveId(visible[0].target.id);
- }
- },
- {
- rootMargin: `-${offsetTop}px 0px -55% 0px`,
- threshold: [0.2, 0.35, 0.5, 0.7],
+ if (!targets.length) return;
+
+ let rafId = 0;
+
+ const updateActiveSection = () => {
+ const baseLine = offsetTop + 500;
+ let nextActiveId = targets[0]?.id ?? "";
+
+ for (const el of targets) {
+ const { top } = el.getBoundingClientRect();
+ if (top <= baseLine) {
+ nextActiveId = el.id;
+ continue;
}
- );
- const targets = allSections
- .map((section) => document.getElementById(section.id))
- .filter(Boolean) as HTMLElement[];
+ break;
+ }
- targets.forEach((el) => observer.observe(el));
+ setActiveId((prev) => (prev === nextActiveId ? prev : nextActiveId));
+ };
- const hash = window.location.hash.replace("#", "");
- const initialActive =
- allSections.find((section) => section.id === hash)?.id ??
- allSections[0]?.id ??
- "";
+ const requestUpdate = () => {
+ if (rafId) return;
- setActiveId(initialActive);
+ rafId = window.requestAnimationFrame(() => { // 예약번호 반환
+ updateActiveSection();
+ rafId = 0;
+ });
+ };
- return () => observer.disconnect();
- }, [allSections, offsetTop]);
+ const hash = window.location.hash.replace("#", "");
+ const initialActive =
+ allSections.find((section) => section.id === hash)?.id ??
+ allSections[0]?.id ??
+ "";
- useEffect(() => {
- if (!activeId) return;
+ setActiveId(initialActive);
+ updateActiveSection();
+
+ window.addEventListener("scroll", requestUpdate, { passive: true });
+ window.addEventListener("resize", requestUpdate);
+
+ return () => {
+ window.removeEventListener("scroll", requestUpdate);
+ window.removeEventListener("resize", requestUpdate);
+
+ if (rafId) {
+ window.cancelAnimationFrame(rafId);
+ }
+ };
+}, [allSections, offsetTop]);
+
+ useEffect(() => {
+ if (!activeId) return;
const parentMenu = menus.find((menu) =>
(menu.sections ?? []).some((section) => section.id === activeId)
@@ -93,13 +119,13 @@ export default function BusinessSubNav({
if (!parentMenu) return;
setOpenMenus(() => {
- const nextOpenState: Record = {};
+ const nextOpenState: Record = {};
- menus.forEach((menu) => {
+ menus.forEach((menu) => {
nextOpenState[menu.key] = menu.key === parentMenu.key;
- });
+ });
- return nextOpenState;
+ return nextOpenState;
});
}, [activeId, menus]);
diff --git a/apps/dm/components/ui/contents/contents-api.ts b/apps/dm/components/ui/contents/contents-api.ts
new file mode 100644
index 000000000..3a7c64639
--- /dev/null
+++ b/apps/dm/components/ui/contents/contents-api.ts
@@ -0,0 +1,26 @@
+// apps/dm/src/entities/content/
+// contents-api.ts
+import { unstable_cache } from "next/cache";
+import { ENDPOINT } from "@/src/shared/config";
+
+
+
+export const getCachedContentDetail = unstable_cache(
+ async (contsSn: number, menuCd: string) => {
+ const baseUrl = process.env.NEXT_PUBLIC_PROXY_HOST || "http://localhost:3015";
+ const res = await fetch(
+ `${baseUrl}${ENDPOINT.CMS_SERVICE.CONTENTS}/${contsSn}?menuCd=${menuCd}`,
+ {
+ next: { revalidate: 3600, tags: [`content:${contsSn}`] },
+ }
+ );
+
+ if (!res.ok) {
+ console.log(res);
+ throw new Error("Failed to fetch content");
+ }
+
+ return res.json();
+ },
+ ["dm-content-detail"],
+);
diff --git a/apps/dm/components/ui/contents/contentsPage.tsx b/apps/dm/components/ui/contents/contentsPage.tsx
new file mode 100644
index 000000000..c823acd42
--- /dev/null
+++ b/apps/dm/components/ui/contents/contentsPage.tsx
@@ -0,0 +1,18 @@
+/**
+ * 컨텐츠 서버 페이지
+ */
+import { getCachedContentDetail } from "./contents-api";
+import { ContentsPageClient } from "./contentsPageClient";
+
+type ContentsPageProps = {
+ contsSn: number;
+ menuCd: string;
+};
+
+async function ContentsPage({ contsSn, menuCd }: ContentsPageProps) {
+ const initialContent = await getCachedContentDetail(contsSn, menuCd);
+
+ return ;
+}
+
+export { ContentsPage };
diff --git a/apps/dm/components/ui/contents/contentsPageClient.tsx b/apps/dm/components/ui/contents/contentsPageClient.tsx
new file mode 100644
index 000000000..97c62bb83
--- /dev/null
+++ b/apps/dm/components/ui/contents/contentsPageClient.tsx
@@ -0,0 +1,37 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+import type { ContentsDetail } from "@/src/entities/content/model/contents-interface";
+
+type ContentsPageClientProps = {
+ initialContent: ContentsDetail;
+};
+
+function ContentsPageClient({ initialContent }: ContentsPageClientProps) {
+ const shadowRootRef = useRef(null);
+
+ useEffect(() => {
+ if (!shadowRootRef.current || !initialContent?.contsCn) return;
+
+ const shadowRoot =
+ shadowRootRef.current.shadowRoot ??
+ shadowRootRef.current.attachShadow({ mode: "open" });
+ shadowRoot.innerHTML = "";
+
+ const styleElement = document.createElement("style");
+ styleElement.textContent = `div { font-size: 16px; line-height: 1.5; }`;
+ shadowRoot.appendChild(styleElement);
+
+ const contentElement = document.createElement("div");
+ contentElement.innerHTML = initialContent.contsCn;
+ shadowRoot.appendChild(contentElement);
+ }, [initialContent]);
+
+ return (
+
+ );
+}
+
+export { ContentsPageClient };
diff --git a/apps/dm/components/ui/contents/index.ts b/apps/dm/components/ui/contents/index.ts
new file mode 100644
index 000000000..82a5dad4f
--- /dev/null
+++ b/apps/dm/components/ui/contents/index.ts
@@ -0,0 +1 @@
+export { ContentsPage } from "./contentsPage";
diff --git a/apps/dm/middleware.ts b/apps/dm/middleware.ts
index f4f145217..c2a228931 100644
--- a/apps/dm/middleware.ts
+++ b/apps/dm/middleware.ts
@@ -60,7 +60,7 @@ export function middleware(req: NextRequest) {
if (pathname.startsWith("/user-service")
|| pathname.startsWith("/cms-service")) {
- let queryParams = searchParams.toString();
+ const queryParams = searchParams.toString();
const requestHeaders = new Headers(req.headers);
const newUrl = `${serverApiUrl}${pathname}${queryParams ? `?${queryParams}` : ""}`;
diff --git a/apps/dm/next.config.ts b/apps/dm/next.config.ts
index 5b5bf85da..bbe67b0ce 100644
--- a/apps/dm/next.config.ts
+++ b/apps/dm/next.config.ts
@@ -48,6 +48,10 @@ const nextConfig: NextConfig = {
source: "/operation-service/:path*",
destination: `${subDomain}/operation-service/:path*`,
},
+ {
+ source: "/cms-service/:path*",
+ destination: `${subDomain}/cms-service/:path*`,
+ },
],
fallback: [],
};
diff --git a/apps/dm/src/entities/content/api/contents-service.ts b/apps/dm/src/entities/content/api/contents-service.ts
new file mode 100644
index 000000000..292d9b795
--- /dev/null
+++ b/apps/dm/src/entities/content/api/contents-service.ts
@@ -0,0 +1,101 @@
+import { client } from "@/src/shared/api";
+import { ENDPOINT } from "@/src/shared/config";
+import type {
+ ContentsService,
+ ContentsParams,
+ ContentsList,
+ ContentsFormData,
+ ContentsVersionParams,
+ ContentsVersionList,
+ ContentsDetail,
+} from "../model/contents-interface";
+
+export class ContentsApiService implements ContentsService {
+ public async getContentsList(params: ContentsParams): Promise {
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.CONTENTS}`,
+ { params },
+ );
+ return response.data;
+ }
+
+ public async getLatestApprovedContentsInfo(
+ contsSn: number,
+ menuCd: string,
+ ): Promise {
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.CONTENTS}/${contsSn}`,
+ { params: { menuCd } },
+ );
+ return response.data;
+ }
+
+ public async getContentsVersionList(
+ params: ContentsVersionParams,
+ ): Promise {
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}`,
+ { params },
+ );
+ return response.data;
+ }
+
+ public async getLatestContentsInfo(
+ contsSn: number,
+ contsVer: number,
+ ): Promise {
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`,
+ );
+ return response.data;
+ }
+
+ public async createContents(data: ContentsFormData): Promise {
+ const response = await client.post(
+ `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}`,
+ data,
+ );
+ return response.data;
+ }
+
+ public async updateContents(
+ contsSn: number,
+ contsVer: number,
+ data: ContentsFormData,
+ ): Promise {
+ const response = await client.put(
+ `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`,
+ data,
+ );
+ return response.data;
+ }
+
+ public async deleteContents(
+ contsSn: number,
+ contsVer: number,
+ ): Promise {
+ await client.delete(
+ `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`,
+ );
+ }
+
+ public async requestApprove(
+ contsSn: number,
+ contsVer: number,
+ ): Promise {
+ await client.put(
+ `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}/approveRequest`,
+ );
+ }
+
+ public async approveContents(
+ contsSn: number,
+ contsVer: number,
+ ): Promise {
+ await client.put(
+ `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}/approve`,
+ );
+ }
+}
+
+export const ContentsApi = new ContentsApiService();
diff --git a/apps/dm/src/entities/content/index.ts b/apps/dm/src/entities/content/index.ts
new file mode 100644
index 000000000..2446dd716
--- /dev/null
+++ b/apps/dm/src/entities/content/index.ts
@@ -0,0 +1,8 @@
+export { ContentsApi } from "./api/contents-service";
+export {
+ CONTENTS_APPROVE_STATUS,
+ contentsFormSchema,
+ type Contents,
+ type ContentsParams,
+ type ContentsFormData,
+} from "./model/contents-interface";
diff --git a/apps/dm/src/entities/content/model/api-interface.ts b/apps/dm/src/entities/content/model/api-interface.ts
new file mode 100644
index 000000000..8218bfb0d
--- /dev/null
+++ b/apps/dm/src/entities/content/model/api-interface.ts
@@ -0,0 +1,34 @@
+interface SortConfig {
+ empty: boolean;
+ sorted: boolean;
+ unsorted: boolean;
+}
+
+interface PageableConfig {
+ sort: SortConfig;
+ offset: number;
+ pageNumber: number;
+ pageSize: number;
+ paged: boolean;
+ unpaged: boolean;
+}
+
+export interface PageableList {
+ content: T[];
+ pageable: PageableConfig;
+ sort: SortConfig;
+ totalPages: number;
+ totalElements: number;
+ number: number;
+ numberOfElements: number;
+ size: number;
+ first: boolean;
+ last: boolean;
+ empty: boolean;
+}
+
+export interface PageableParams {
+ page?: number;
+ size?: number;
+ sort?: string;
+}
diff --git a/apps/dm/src/entities/content/model/contents-interface.ts b/apps/dm/src/entities/content/model/contents-interface.ts
new file mode 100644
index 000000000..474dfe97c
--- /dev/null
+++ b/apps/dm/src/entities/content/model/contents-interface.ts
@@ -0,0 +1,105 @@
+import { z } from "zod";
+import type { PageableList, PageableParams } from "./api-interface";
+
+export const CONTENTS_APPROVE_STATUS = {
+ DRAFT: "01",
+ REQUESTED: "02",
+ APPROVED: "99",
+} as const;
+
+export type ContentsApproveStatus =
+ (typeof CONTENTS_APPROVE_STATUS)[keyof typeof CONTENTS_APPROVE_STATUS];
+
+export interface ContentsParams extends PageableParams {
+ contsTtl?: string;
+ aprvContsVer?: number;
+ useYn?: string;
+}
+
+export interface ContentsVersionParams extends PageableParams {
+ contsSn?: number;
+ contsCn?: string;
+ aprvCd?: ContentsApproveStatus;
+ cclCd?: string;
+ koglCd?: string;
+ tag?: string;
+ useYn?: string;
+}
+
+export interface ContentsVersion {
+ contsSn: number;
+ contsVer: number;
+ contsCn: string;
+ useYn: string;
+}
+
+export const contentsSchema = z.object({
+ contsSn: z.number(),
+ contsTtl: z.string(),
+ aprvContsVer: z.number(),
+ useYn: z.string(),
+ mdfcnDt: z.string(),
+ mdfr: z.string(),
+ mdfrNm: z.string(),
+ aprvDt: z.string(),
+ autzrNm: z.string(),
+ workAprvCd: z.string(),
+ workAprvNm: z.string(),
+ workContsVer: z.number(),
+});
+
+export const contentsDetailSchema = z.object({
+ contsSn: z.number(),
+ contsVer: z.number(),
+ contsTtl: z.string(),
+ contsCn: z.string(),
+ contsRmrk: z.string().nullish(),
+ aprvCd: z.enum([
+ CONTENTS_APPROVE_STATUS.DRAFT,
+ CONTENTS_APPROVE_STATUS.REQUESTED,
+ CONTENTS_APPROVE_STATUS.APPROVED,
+ ]),
+ cclCd: z.string().nullish(),
+ koglCd: z.string().nullish(),
+ tag: z.string().nullish(),
+ aprvDt: z.string(),
+ autzr: z.string(),
+ useYn: z.string(),
+ menuPicUserUnqIdNm: z.string().nullish(),
+});
+
+export const contentsFormSchema = contentsDetailSchema.omit({
+ contsVer: true,
+ aprvDt: true,
+ autzr: true,
+});
+
+export type Contents = z.infer;
+export type ContentsDetail = z.infer;
+export type ContentsFormData = z.infer;
+export type ContentsList = PageableList;
+export type ContentsVersionList = PageableList;
+
+export interface ContentsService {
+ getContentsList(params: ContentsParams): Promise;
+ getLatestApprovedContentsInfo(
+ contsSn: number,
+ menuCd: string,
+ ): Promise;
+ getContentsVersionList(
+ params: ContentsVersionParams,
+ ): Promise;
+ getLatestContentsInfo(
+ contsSn: number,
+ contsVer: number,
+ ): Promise;
+ createContents(data: ContentsFormData): Promise;
+ updateContents(
+ contsSn: number,
+ contsVer: number,
+ data: ContentsFormData,
+ ): Promise;
+ deleteContents(contsSn: number, contsVer: number): Promise;
+ requestApprove(contsSn: number, contsVer: number): Promise;
+ approveContents(contsSn: number, contsVer: number): Promise;
+}
diff --git a/apps/st/src/components/css/DataGrid.css b/apps/st/src/components/css/DataGrid.css
index 190d41a79..a79e3ebd4 100644
--- a/apps/st/src/components/css/DataGrid.css
+++ b/apps/st/src/components/css/DataGrid.css
@@ -195,7 +195,7 @@
* .rg-grid … 특이도로 덮어씀.
*/
.rg-grid .rg-header .rg-table .rg-header-group-cell {
- background-color: #466995 !important;
+ background-color: #5987bf !important;
color: #ffffff !important;
}
.rg-grid .rg-header .rg-table {
@@ -215,6 +215,8 @@
color: #ffffff !important;
}
+.rg-header .rg-table tr td:hover { background: #5987bf; }
+
[data-theme="black"] .rg-grid .rg-header .rg-table .rg-header-group-cell {
background-color: #3c4043 !important;
color: #ffffff !important;
@@ -235,6 +237,11 @@
background-color: #62686d !important;
color: #ffffff !important;
}
+[data-theme="black"] .rg-header .rg-table tr td:hover {
+ background-color: #62686d !important;
+ color: #ffffff !important;
+}
+
[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell {
background-color: #545556 !important;
color: #ffffff !important;
diff --git a/apps/st/src/components/module/ExceptionProcessingHistoryDialog.tsx b/apps/st/src/components/module/ExceptionProcessingHistoryDialog.tsx
index 04cd06dc9..483346a6c 100644
--- a/apps/st/src/components/module/ExceptionProcessingHistoryDialog.tsx
+++ b/apps/st/src/components/module/ExceptionProcessingHistoryDialog.tsx
@@ -565,7 +565,7 @@ export function ExceptionProcessingHistoryDialog({
onClick={handleOpenRejectReasonDialog}
disabled={rejectSubmitting}
>
- 반려
+ 일괄 반려
- setPrxyCtrtCn(e.target.value)}
- className="!text-[1.3rem]"
- placeholder="YYYYMMDD~YYYYMMDD (예: 20240101~20281231)"
- autoComplete="off"
- disabled={isSubmitting}
- />
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx
index 471547ec5..e8c71de1b 100644
--- a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx
+++ b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
import { useConfirm } from '@frontend-opensource/use-react-hooks';
-import { useAlertStore, useGlobalProgressStore } from '@/shared/lib';
+import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from '@/shared/lib';
import { statisticsInputSewSupplyRateApi } from '@/entities/statistics/api/input/statistics-input-sew-supply-rate-service';
import { statisticsInputPublicSewageFacilityApiService } from '@/entities/statistics/api/input/statistics-public-sewage-facility-input-service';
import { statisticsApi } from '@/entities/statistics/api/statistics-service';
@@ -26,7 +26,7 @@ import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from '
import { getUserInfo } from '../../../../../../packages/common/components/src/api';
-import { MainFacilityMappingSaveDialog } from './MainFacilityMappingSaveDialog';
+import { MainFacilityMappingSaveDialog, type MainFacilityMappingSaveDialogValues } from './MainFacilityMappingSaveDialog';
import SubmitDailog from './SubmitDailog';
import CancelSubmitButton from '@/components/button/CancelSubmitButton';
@@ -145,6 +145,7 @@ const PublicSewageFacilityInp = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const { confirm } = useConfirm();
const { isAdminInfo } = useAdminStore();
+ const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate();
const showGlobalProgress = useGlobalProgressStore((state) => state.show);
const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress);
const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel);
@@ -415,6 +416,13 @@ const PublicSewageFacilityInp = () => {
.filter((sn) => sn !== '');
const wholNopeFromRow = Number(selectedMain.wholNope ?? 0);
+ const deleteOnly = saveList.length === 0 && deleteList.length > 0;
+ const nothingToPersist = saveList.length === 0 && deleteList.length === 0;
+ if (nothingToPersist) {
+ alert('저장할 변경 사항이 없습니다.');
+ return;
+ }
+
mainFacilityMappingSaveDraftRef.current = {
statsYr: lastSearchedValues.year ?? '',
sewerBzmnCd: lastSearchedValues.bsnmCd ?? '',
@@ -429,6 +437,16 @@ const PublicSewageFacilityInp = () => {
initialWholNope: Number.isFinite(wholNopeFromRow) ? wholNopeFromRow : 0,
initialPrxyCtrtCn: String(selectedMain.prxyCtrtCn ?? ''),
};
+
+ if (deleteOnly) {
+ const draft = mainFacilityMappingSaveDraftRef.current;
+ const ok = await runMainFacilityMappingSave(draft, null);
+ if (ok) {
+ mainFacilityMappingSaveDraftRef.current = null;
+ }
+ return;
+ }
+
setMainFacilityMappingSaveDialogKey((k) => k + 1);
setMainFacilityMappingSaveDialogOpen(true);
return;
@@ -671,42 +689,40 @@ const PublicSewageFacilityInp = () => {
}
};
- const handleMainFacilityMappingSaveConfirm = async (input: {
- prxyBzentyNmCn: string;
- cnsgnCstVl: number;
- wholNope: number;
- prxyCtrtCn: string;
- }) => {
- const draft = mainFacilityMappingSaveDraftRef.current;
- if (!draft || !searchParamRef.current || !lastSearchedValues) {
- setMainFacilityMappingSaveDialogOpen(false);
- mainFacilityMappingSaveDraftRef.current = null;
- return;
+ const runMainFacilityMappingSave = async (
+ draft: MainFacilityMappingSaveDraft,
+ dialogInput: MainFacilityMappingSaveDialogValues | null,
+ ): Promise => {
+ if (!searchParamRef.current || !lastSearchedValues) {
+ return false;
}
try {
showGlobalProgress({ label: '저장 진행 중...', progress: 0 });
const userInfo = await getUserInfo();
- const param: PublicSewageMainFacilityMappingSaveRequest = {
+ const base = {
statsYr: draft.statsYr,
sewerBzmnCd: draft.sewerBzmnCd,
admdstCd: draft.admdstCd,
admdstNm: draft.admdstNm,
mainFcltSn: draft.mainFcltSn,
mainFcltNm: draft.mainFcltNm,
- prxyBzentyNmCn: input.prxyBzentyNmCn,
- cnsgnCstVl: input.cnsgnCstVl,
- wholNope: input.wholNope,
- prxyCtrtCn: input.prxyCtrtCn,
userId: userInfo.userUnqId,
saveList: draft.saveList,
deleteList: draft.deleteList,
};
+ const param: PublicSewageMainFacilityMappingSaveRequest = dialogInput
+ ? {
+ ...base,
+ prxyBzentyNmCn: dialogInput.prxyBzentyNmCn,
+ cnsgnCstVl: dialogInput.cnsgnCstVl,
+ wholNope: dialogInput.wholNope,
+ prxyCtrtCn: dialogInput.prxyCtrtCn,
+ }
+ : { ...base };
const response =
await statisticsInputPublicSewageFacilityApiService.saveMainFacilityMapping(param);
if (response.status === 'success') {
setGlobalProgress(50);
- setMainFacilityMappingSaveDialogOpen(false);
- mainFacilityMappingSaveDraftRef.current = null;
await loadPublicSewageFacilityActiveTabData(
'ST00060002',
searchParamRef.current,
@@ -714,16 +730,32 @@ const PublicSewageFacilityInp = () => {
);
setGlobalProgress(100);
alert('저장되었습니다.');
- } else {
- alert(response.message || '데이터 저장 중 오류가 발생했습니다.');
+ return true;
}
+ alert(response.message || '데이터 저장 중 오류가 발생했습니다.');
+ return false;
} catch {
alert('데이터 저장 중 오류가 발생했습니다.');
+ return false;
} finally {
hideGlobalProgress();
}
};
+ const handleMainFacilityMappingSaveConfirm = async (input: MainFacilityMappingSaveDialogValues) => {
+ const draft = mainFacilityMappingSaveDraftRef.current;
+ if (!draft || !searchParamRef.current || !lastSearchedValues) {
+ setMainFacilityMappingSaveDialogOpen(false);
+ mainFacilityMappingSaveDraftRef.current = null;
+ return;
+ }
+ const ok = await runMainFacilityMappingSave(draft, input);
+ if (ok) {
+ setMainFacilityMappingSaveDialogOpen(false);
+ mainFacilityMappingSaveDraftRef.current = null;
+ }
+ };
+
const handleSearch = async (values: SearchFormValues) => {
if (!values.sido || values.sido === 'ALL') {
alert('시도를 선택해주세요.');
@@ -928,7 +960,14 @@ const PublicSewageFacilityInp = () => {
inputPeriodEnterable,
isAdminInfo,
);
- const { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard } = activeTabToolbar;
+ const {
+ showSaveInToolbar: baseShowSaveInToolbar,
+ showEnterableToolbarButtons: baseShowEnterableToolbarButtons,
+ showActionToolbarCard: baseShowActionToolbarCard,
+ } = activeTabToolbar;
+ const showSaveInToolbar = baseShowSaveInToolbar && toolbarGroupAllowed;
+ const showEnterableToolbarButtons = baseShowEnterableToolbarButtons && toolbarGroupAllowed;
+ const showActionToolbarCard = baseShowActionToolbarCard && toolbarGroupAllowed;
const isMainFacilityTab = activeTab === 'ST00060002';
const openVerificationHistoryDialog = () => {
diff --git a/apps/st/src/components/statistics/input/SewagePipeInp.tsx b/apps/st/src/components/statistics/input/SewagePipeInp.tsx
index 34314a12b..963be10fb 100644
--- a/apps/st/src/components/statistics/input/SewagePipeInp.tsx
+++ b/apps/st/src/components/statistics/input/SewagePipeInp.tsx
@@ -2,7 +2,7 @@ import { useEffect, useLayoutEffect,useMemo, useRef, useState } from "react";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
-import { useAlertStore, useGlobalProgressStore } from "@/shared/lib";
+import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
import {
STATISTICS_INPUT_URL_PATH,
verificationSearchToFormValues,
@@ -375,6 +375,7 @@ const SewagePipeInp = () => {
const hideGlobalProgress = useGlobalProgressStore((state) => state.hide);
const globalProgressVisible = useGlobalProgressStore((state) => state.visible);
const { isAdminInfo } = useAdminStore();
+ const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate();
const getGridDataRef = useRef<(() => Record[]) | null>(null);
const getFactInstallGridDataRef = useRef<(() => Record[]) | null>(null);
@@ -1783,10 +1784,12 @@ const SewagePipeInp = () => {
showEnterableToolbarButtons: baseShowEnterableToolbarButtons,
showActionToolbarCard: baseShowActionToolbarCard,
} = activeTabToolbar;
- const showSaveInToolbar = !hideToolbarForConnectionTab && baseShowSaveInToolbar;
+ const showSaveInToolbar =
+ !hideToolbarForConnectionTab && baseShowSaveInToolbar && toolbarGroupAllowed;
const showEnterableToolbarButtons =
- !hideToolbarForConnectionTab && baseShowEnterableToolbarButtons;
- const showActionToolbarCard = !hideToolbarForConnectionTab && baseShowActionToolbarCard;
+ !hideToolbarForConnectionTab && baseShowEnterableToolbarButtons && toolbarGroupAllowed;
+ const showActionToolbarCard =
+ !hideToolbarForConnectionTab && baseShowActionToolbarCard && toolbarGroupAllowed;
return (
<>
diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx
index 928e4b2ca..22b160656 100644
--- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx
+++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx
@@ -2,7 +2,7 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
-import { useAlertStore, useGlobalProgressStore } from "@/shared/lib";
+import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
import {
STATISTICS_INPUT_URL_PATH,
verificationSearchToFormValues,
@@ -192,6 +192,7 @@ const SeweragePenetrationRateInp = () => {
}, [activeTabId]);
const { isAdminInfo } = useAdminStore();
+ const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate();
const getGridDataRef = useRef<(() => Record[]) | null>(null);
@@ -710,12 +711,15 @@ const SeweragePenetrationRateInp = () => {
? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL')
: true;
- const showSaveInToolbar = (isEnterable || isAdminInfo) && gugunOkOrNotDoList;
- const showEnterableToolbarButtons = isEnterable && gugunOkOrNotDoList;
+ const baseShowSaveInToolbar = (isEnterable || isAdminInfo) && gugunOkOrNotDoList;
+ const baseShowEnterableToolbarButtons = isEnterable && gugunOkOrNotDoList;
+ const showSaveInToolbar = baseShowSaveInToolbar && toolbarGroupAllowed;
+ const showEnterableToolbarButtons = baseShowEnterableToolbarButtons && toolbarGroupAllowed;
/** 카드는 저장/제출줄/제출취소 중 하나라도 보일 때만 (관리자만으로 빈 카드 방지) */
const showActionToolbarCard =
rowData.length > 0 &&
- (showSaveInToolbar ||
+ toolbarGroupAllowed &&
+ (baseShowSaveInToolbar ||
(statusInfo?.laeSbmsnSttsCd === '2' && (isAdminInfo || (isEnterable && gugunOkOrNotDoList))));
return (
diff --git a/apps/st/src/components/statistics/input/StatsInputStatusInp.tsx b/apps/st/src/components/statistics/input/StatsInputStatusInp.tsx
index af78598e7..db04bc957 100644
--- a/apps/st/src/components/statistics/input/StatsInputStatusInp.tsx
+++ b/apps/st/src/components/statistics/input/StatsInputStatusInp.tsx
@@ -1,9 +1,10 @@
-import { useCallback, useEffect, useRef, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { RealGridReact } from "realgrid-react";
import useSWR, { useSWRConfig } from "swr";
-import { useAlertStore } from "@/shared/lib";
+import { useAlertStore, useStatisticsInputStatusConfirmGroupGate } from "@/shared/lib";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import { StatisticsInputStatusApi } from "@/entities/statistics/api/input/statistics-input-status-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
@@ -17,8 +18,8 @@ import type {
StatsInputStatusListRequestParams,
} from "@/entities/statistics/model/input/statistics-input-status-interface";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
-import { LoadingWithAnimation } from "@common/components";
+import { LoadingWithAnimation } from "@common/components";
import ApproveButton from "@/components/button/ApproveButton";
import DataGrid from "@/components/module/DataGrid";
import type { DataCellClickCallbackParams } from "@/components/type/DataGrid.type";
@@ -41,7 +42,9 @@ const getStatsInputStatusFetcher = async (
// 하수도 통계 관리 > 00. 통계 입력 현황
const StatsInputStatusInp = () => {
+
const { setMessage: alert } = useAlertStore((state) => state);
+ const { confirm } = useConfirm();
const activeTabId = useTabStore((state) => state.activeTabId);
const [rowData, setRowData] = useState[]>(
[],
@@ -58,6 +61,8 @@ const StatsInputStatusInp = () => {
const [exceptionHistoryContext, setExceptionHistoryContext] = useState<
ExceptionProcessingHistoryQueryContext | null
>(null);
+ const [isApproveLoading, setIsApproveLoading] = useState(false);
+ const confirmGroupToolbarAllowed = useStatisticsInputStatusConfirmGroupGate();
const swrKey = searchParams
? (["stats-input-status", searchParams] as const)
@@ -140,6 +145,7 @@ const StatsInputStatusInp = () => {
inputPeriodCurrentRef.current = false;
setExceptionHistoryOpen(false);
setExceptionHistoryContext(null);
+ setIsApproveLoading(false);
};
const approveFunc = async () => {
@@ -152,15 +158,20 @@ const StatsInputStatusInp = () => {
return;
}
- // const hasInvalidStatus = rowData.some((row) =>
- // ["0", "1", "4"].includes(String(row.laeSbmsnSttsCd ?? "")),
- // );
+ const hasInvalidStatus = rowData.some((row) =>
+ ["0", "1", "4"].includes(String(row.laeSbmsnSttsCd ?? "")),
+ );
- // if (hasInvalidStatus) {
- // alert("미작성이나 미제출, 혹은 보완요청(반려) 상태인 항목이 있습니다.");
- // return;
- // }
+ if (hasInvalidStatus) {
+ alert("미작성이나 미제출, 혹은 보완요청(반려) 상태인 항목이 있습니다.");
+ return;
+ }
+ if (!(await confirm("조회된 통계 항목을 승인하시겠습니까?"))) {
+ return;
+ }
+
+ setIsApproveLoading(true);
try {
const userInfo = await getUserInfo();
const mdfrId = (userInfo.userUnqId ?? "").trim();
@@ -186,7 +197,7 @@ const StatsInputStatusInp = () => {
}
const response = await statisticsApi.approveStatsInputStatus({ list: approveItems }, "INPUT");
- if (response.status === "success") {
+ if (response.resultCode.toLocaleLowerCase() === "success") {
if (searchParams) {
await mutate(["stats-input-status", searchParams]);
}
@@ -196,6 +207,8 @@ const StatsInputStatusInp = () => {
}
} catch {
alert("승인 처리 중 오류가 발생했습니다.");
+ } finally {
+ setIsApproveLoading(false);
}
};
@@ -239,8 +252,21 @@ const StatsInputStatusInp = () => {
const gugunOkOrNotDoList = doList.includes(lastSearchedValues?.bsnmCd || "")
? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== "ALL")
: true;
+
+ const allRowsLaeSbmsnApproved = useMemo(
+ () =>
+ rowData.length > 0 &&
+ rowData.every((row) => String(row.laeSbmsnSttsCd ?? "") === "3"),
+ [rowData],
+ );
+
const showActionToolbarCard =
- hasCompletedSearch && rowData.length > 0 && gugunOkOrNotDoList && inputPeriodCurrentRef.current;
+ hasCompletedSearch &&
+ rowData.length > 0 &&
+ gugunOkOrNotDoList &&
+ inputPeriodCurrentRef.current &&
+ !allRowsLaeSbmsnApproved &&
+ confirmGroupToolbarAllowed;
return (
<>
@@ -253,7 +279,7 @@ const StatsInputStatusInp = () => {
)}
- {isLoading && (
+ {(isLoading || isApproveLoading) && (
diff --git a/apps/st/src/components/statistics/management/StatisticsInputManagement.tsx b/apps/st/src/components/statistics/management/StatisticsInputManagement.tsx
index 2ef6b312e..cb63324eb 100644
--- a/apps/st/src/components/statistics/management/StatisticsInputManagement.tsx
+++ b/apps/st/src/components/statistics/management/StatisticsInputManagement.tsx
@@ -1,4 +1,4 @@
-import { useRef, useState } from "react";
+import { useCallback, useRef, useState } from "react";
import { useAlertStore } from "@/shared/lib";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
@@ -15,7 +15,7 @@ import {
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
-import type { ColumnConfig } from "@/components/type/DataGrid.type";
+import type { ColumnConfig, DataCellClickCallbackParams } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { toGridRows } from "@/components/util/grid/gridFunction";
@@ -53,6 +53,8 @@ function formatYmdForDisplay(ymd: string): string {
return `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`;
}
+const PERIOD_DETAIL_FIELD = "detailInfo";
+
const StatisticsInputManagement = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const [isLoading, setIsLoading] = useState(false);
@@ -88,6 +90,7 @@ const StatisticsInputManagement = () => {
seType: seTypeToDisplayLabel(row.seType),
inptBgngYmd: formatYmdForDisplay(row.inptBgngYmd),
inptEndYmd: formatYmdForDisplay(row.inptEndYmd),
+ [PERIOD_DETAIL_FIELD]: "상세정보",
}));
setRowData(toGridRows(rowsForGrid));
} else {
@@ -119,48 +122,59 @@ const StatisticsInputManagement = () => {
setPeriodDialogOpen(true);
};
- const handleRowClick = async (row: Record ) => {
- const seCdRaw = row.seCd;
- const seCdNum =
- typeof seCdRaw === "number"
- ? seCdRaw
- : parseInt(String(seCdRaw ?? ""), 10);
- const seCd = Number.isFinite(seCdNum) ? seCdNum : 0;
- const statsYr = String(row.statsYr ?? "");
- const seType = rowSeTypeToCode(row);
+ const openPeriodDetailForRow = useCallback(
+ async (row: Record) => {
+ const seCdRaw = row.seCd;
+ const seCdNum =
+ typeof seCdRaw === "number"
+ ? seCdRaw
+ : parseInt(String(seCdRaw ?? ""), 10);
+ const seCd = Number.isFinite(seCdNum) ? seCdNum : 0;
+ const statsYr = String(row.statsYr ?? "");
+ const seType = rowSeTypeToCode(row);
- try {
- setIsLoading(true);
- const res = await statisticsApi.getStatsInputPeriodDetail({
- statsYr,
- seType,
- seCd,
- });
+ try {
+ setIsLoading(true);
+ const res = await statisticsApi.getStatsInputPeriodDetail({
+ statsYr,
+ seType,
+ seCd,
+ });
- if (res.status !== "success" || !res.data) {
- alert(res.message || "상세 정보를 조회하지 못했습니다.");
- return;
+ if (res.status !== "success" || !res.data) {
+ alert(res.message || "상세 정보를 조회하지 못했습니다.");
+ return;
+ }
+
+ const detail = res.data;
+ setPeriodDialogMode("edit");
+ setPeriodDialogInitial({
+ statsYr: String(detail.statsYr ?? statsYr),
+ seType: detail.seType === "VRFC" ? "VRFC" : "INPT",
+ seCd:
+ typeof detail.seCd === "number" && Number.isFinite(detail.seCd)
+ ? detail.seCd
+ : seCd,
+ inptBgngYmd: parseYmdToDate(detail.inptBgngYmd),
+ inptEndYmd: parseYmdToDate(detail.inptEndYmd),
+ });
+ setPeriodDialogOpen(true);
+ } catch {
+ alert("상세 정보 조회 중 오류가 발생했습니다.");
+ } finally {
+ setIsLoading(false);
}
+ },
+ [alert],
+ );
- const detail = res.data;
- setPeriodDialogMode("edit");
- setPeriodDialogInitial({
- statsYr: String(detail.statsYr ?? statsYr),
- seType: detail.seType === "VRFC" ? "VRFC" : "INPT",
- seCd:
- typeof detail.seCd === "number" && Number.isFinite(detail.seCd)
- ? detail.seCd
- : seCd,
- inptBgngYmd: parseYmdToDate(detail.inptBgngYmd),
- inptEndYmd: parseYmdToDate(detail.inptEndYmd),
- });
- setPeriodDialogOpen(true);
- } catch {
- alert("상세 정보 조회 중 오류가 발생했습니다.");
- } finally {
- setIsLoading(false);
- }
- };
+ const handlePeriodDetailCellClick = useCallback(
+ ({ fieldName, row }: DataCellClickCallbackParams) => {
+ if (fieldName !== PERIOD_DETAIL_FIELD) return;
+ void openPeriodDetailForRow(row);
+ },
+ [openPeriodDetailForRow],
+ );
const columns: ColumnConfig[] = [
{
@@ -193,6 +207,14 @@ const StatisticsInputManagement = () => {
header: { text: "종료일" },
editable: false,
},
+ {
+ name: PERIOD_DETAIL_FIELD,
+ fieldName: PERIOD_DETAIL_FIELD,
+ header: { text: "상세정보" },
+ editable: false,
+ width: 118,
+ styleName: "rg-cell-outline-action-btn",
+ },
];
const columnLayout = [
@@ -201,6 +223,7 @@ const StatisticsInputManagement = () => {
"seCd",
"inptBgngYmd",
"inptEndYmd",
+ PERIOD_DETAIL_FIELD,
];
return (
@@ -232,7 +255,7 @@ const StatisticsInputManagement = () => {
setRowData={setRowData}
isExcelImport={false}
isExcelExport={false}
- onRowClick={handleRowClick}
+ onDataCellClick={handlePeriodDetailCellClick}
/>
{
+ const { setMessage: alert } = useAlertStore((state) => state);
const [rowData, setRowData] = useState[]>([]);
+ const [isLoading, setIsLoading] = useState(false);
const columns: ColumnConfig[] = [
- { name: 'sido', fieldName: '시도', header: { text: '시도' }, mergeCells: true, editable: false, sortdisable: true },
- { name: 'sgg', fieldName: '시군구', header: { text: '시군구' }, mergeCells: true, editable: false, sortdisable: true },
- { name: '구분', fieldName: '구분', header: { text: '구분' }, editable: false, sortdisable: true },
- { name: '광역자치단체', fieldName: '광역자치단체', header: { text: '광역자치단체' }, editable: false, sortdisable: true },
- { name: '공단', fieldName: '공단', header: { text: '공단' }, editable: false, sortdisable: true },
- { name: '처리내역', fieldName: '처리내역', header: { text: '처리내역' }, editable: false, sortdisable: true },
+ { name: "statsYr", fieldName: "statsYr", header: { text: "연도" }, editable: false },
+ { name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
+ { name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
+ { name: "sewerBzmnCd", fieldName: "sewerBzmnCd", header: { text: "하수사업자코드" }, editable: false },
+ { name: "statsCd", fieldName: "statsCd", header: { text: "통계코드" }, editable: false },
+ { name: "menuNm", fieldName: "menuNm", header: { text: "통계항목" }, editable: false },
+ { name: "chckerOpinionSm", fieldName: "chckerOpinionSm", header: { text: "검토자의견" }, editable: false },
+ { name: "wrterOpinionSm", fieldName: "wrterOpinionSm", header: { text: "처리내역" }, editable: false },
];
const columnLayout = [
- 'sido',
- 'sgg',
- '구분',
- {
- name: 'groupA',
- hideChildHeaders: false,
- header: { text: '검토내역' },
- items: ['광역자치단체', '공단', '처리내역'],
- },
+ "statsYr",
+ "ctpvNm",
+ "sggNm",
+ "sewerBzmnCd",
+ "statsCd",
+ "menuNm",
+ "chckerOpinionSm",
+ "wrterOpinionSm",
];
+ const handleSearch = async (values: SearchFormValues) => {
+ if (!values.year?.trim()) {
+ alert("년도를 선택해주세요.");
+ return;
+ }
+
+ try {
+ setIsLoading(true);
+ const res = await statisticsApi.getStatiscicsManagementReviewHistory({
+ statsYr: values.year.trim(),
+ ...(values.bsnmCd?.trim() ? { sewerBzmnCd: values.bsnmCd.trim() } : {}),
+ ...(values.sido && values.sido !== "ALL" ? { ctpvNm: values.sido } : {}),
+ });
+
+ if (res.status === "success" && Array.isArray(res.data)) {
+ setRowData(toGridRows(res.data));
+ } else {
+ setRowData([]);
+ alert(res.message || "조회된 데이터가 없습니다.");
+ }
+ } catch {
+ setRowData([]);
+ alert("데이터 조회 중 오류가 발생했습니다.");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleReset = () => {
+ setRowData([]);
+ };
+
return (
- <>
-
-
- >
+
+
+ {isLoading && (
+
+ )}
+
+
);
};
-export default StatisticsReviewHistory;
\ No newline at end of file
+export default StatisticsReviewHistory;
diff --git a/apps/st/src/components/statistics/review/StatsInputStatusRvw.tsx b/apps/st/src/components/statistics/review/StatsInputStatusRvw.tsx
index 73258a130..8241d9bcd 100644
--- a/apps/st/src/components/statistics/review/StatsInputStatusRvw.tsx
+++ b/apps/st/src/components/statistics/review/StatsInputStatusRvw.tsx
@@ -1,4 +1,5 @@
import { useCallback, useMemo, useState } from "react";
+import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import DataGrid from "@/components/module/DataGrid";
@@ -41,7 +42,7 @@ const checkSubmitStatusCd = (code: string) => {
};
const isStatsInputStatusRowApprovable = (row: Record) =>
- String(row.submitStatusCd ?? "") === "3" && String(row.prvnStatusCd ?? "") === "2";
+ String(row.submitStatusCd ?? "") === "3" && (String(row.prvnStatusCd ?? "") === "2" || String(row.prvnStatusCd ?? "") === "3");
const mapSewerageListToGridRows = (rawList: StatsInputStatusReviewItem[]) =>
rawList.map((r) => ({
@@ -54,9 +55,11 @@ const mapSewerageListToGridRows = (rawList: StatsInputStatusReviewItem[]) =>
// 하수도 통계 검토 00. 통계 입력 현황
const StatsInputStatusRvw = () => {
const { setMessage: alert } = useAlertStore((state) => state);
+ const { confirm } = useConfirm();
const [rowData, setRowData] = useState[]>([]);
const [historyRowData, setHistoryRowData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
+ const [isApproveLoading, setIsApproveLoading] = useState(false);
const [searchedYear, setSearchedYear] = useState("");
const [lastSearchedValues, setLastSearchedValues] = useState(null);
const [hasCompletedSearch, setHasCompletedSearch] = useState(false);
@@ -108,6 +111,7 @@ const StatsInputStatusRvw = () => {
setSearchedYear("");
setLastSearchedValues(null);
setHasCompletedSearch(false);
+ setIsApproveLoading(false);
};
const handleSearch = async (values: SearchFormValues) => {
@@ -158,11 +162,16 @@ const StatsInputStatusRvw = () => {
return;
}
- // if (!rowData.every(isStatsInputStatusRowApprovable)) {
- // alert("지자체 진행상태가 승인완료이고 진행상태가 제출완료인 항목만 승인할 수 있습니다.");
- // return;
- // }
+ if (!rowData.every(isStatsInputStatusRowApprovable)) {
+ alert("지자체 진행상태가 승인완료이고 진행상태가 제출완료인 항목만 승인할 수 있습니다.");
+ return;
+ }
+ if (!(await confirm("조회된 통계 항목을 승인하시겠습니까?"))) {
+ return;
+ }
+
+ setIsApproveLoading(true);
try {
const userInfo = await getUserInfo();
const mdfrId = (userInfo.userUnqId ?? "").trim();
@@ -188,7 +197,7 @@ const StatsInputStatusRvw = () => {
}
const response = await statisticsApi.approveStatsInputStatus({ list: approveItems }, "REVIEW");
- if (response.status === "success") {
+ if (response.resultCode.toLocaleLowerCase() === "success") {
try {
const listResponse = await statisticsManagementApi.getStatsInputStatusList({
ctpvNm: lastSearchedValues.sido,
@@ -209,6 +218,8 @@ const StatsInputStatusRvw = () => {
}
} catch {
alert("승인 처리 중 오류가 발생했습니다.");
+ } finally {
+ setIsApproveLoading(false);
}
};
@@ -223,11 +234,20 @@ const StatsInputStatusRvw = () => {
[rowData],
);
+ /** 진행상태(prvn) 코드가 모두 승인완료(3) */
+ const allPrvnStatusApproved = useMemo(
+ () =>
+ rowData.length > 0 &&
+ rowData.every((row) => String(row.prvnStatusCd ?? "") === "3"),
+ [rowData],
+ );
+
const showActionToolbarCard =
hasCompletedSearch &&
rowData.length > 0 &&
gugunOkOrNotDoList &&
- !allSubmitStatusApproved;
+ allSubmitStatusApproved &&
+ !allPrvnStatusApproved;
const openReviewHistoryForRow = useCallback(
async (row: Record) => {
@@ -282,7 +302,7 @@ const StatsInputStatusRvw = () => {
)}
- {isLoading && (
+ {(isLoading || isApproveLoading) && (
diff --git a/apps/st/src/components/statistics/verification/StatisticsVerificationReport.tsx b/apps/st/src/components/statistics/verification/StatisticsVerificationReport.tsx
index 36274ddcd..3e537c987 100644
--- a/apps/st/src/components/statistics/verification/StatisticsVerificationReport.tsx
+++ b/apps/st/src/components/statistics/verification/StatisticsVerificationReport.tsx
@@ -131,12 +131,12 @@ const DEFAULT_FILTERS: StatisticsVerificationFilterState = {
statsYr: String(dayjs().year() - 1),
sewerBzmnCd: null,
statsCd: null,
- laeSbmsnSttsCd: "2",
+ laeSbmsnSttsCd: "3",
};
const DEFAULT_SUB_FILTERS: StatisticsVerificationSubFilterState = {
...DEFAULT_FILTERS,
- prcsYnList: "1,2,3",
+ prcsYnList: "1,2",
pbcpAprvYn: "N",
};
@@ -468,8 +468,6 @@ function StatisticsVerificationReport() {
async function handleReject() {
const commonParam = {
statsYr: filters.statsYr,
- sewerBzmnCd: filters.sewerBzmnCd ?? "",
- statsCd: filters.statsCd ?? "",
mdfrId: userInfo?.userUnqId ?? "",
};
@@ -496,6 +494,8 @@ function StatisticsVerificationReport() {
...commonParam,
vrfcSn: Number.isNaN(vrfcSn) ? 0 : vrfcSn,
rjctOpnnCn: String(row.rjctOpnnCn ?? ""),
+ sewerBzmnCd: String(row.sewerBzmnCd ?? ""),
+ statsCd: String(row.statsCd ?? ""),
};
});
diff --git a/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts b/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts
index a576c5caa..d0100b790 100644
--- a/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts
+++ b/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts
@@ -1,5 +1,7 @@
import { ValueType } from "realgrid";
+import type { ColumnInfoList } from "@/components/type/DataGrid.type";
+
/**
* 하수도 통계 관리 > 02. 하수관로 (합계)
* 항목은 검토(SewagePipeRvwColumn)와 동일하나 footer·mergeCells·구분(displayYear)·비고(rmrk) 제외.
@@ -976,16 +978,18 @@ export const getST00020003Config = (statsYr: string | number) => {
};
};
-export const SewagePipeColumnInfoListTot = rawSewagePipeTotList.map((tab) => ({
+export const SewagePipeColumnInfoListTot: ColumnInfoList[] = rawSewagePipeTotList.map((tab) => ({
...tab,
columns: tab.columns.map((col) => ({ ...col, editable: false as const })),
}));
-SewagePipeColumnInfoListTot.splice(2, 0, {
+const st00020003PlaceholderTab: ColumnInfoList = {
tabKey: "ST00020003",
tabLabel: "관종별 현황",
rowStartNum: 2,
fixedColumns: 2,
- columns: [],
- columnLayout: [],
-} as any);
\ No newline at end of file
+ columns: [],
+ columnLayout: [],
+};
+
+SewagePipeColumnInfoListTot.splice(2, 0, st00020003PlaceholderTab);
\ No newline at end of file
diff --git a/apps/st/src/entities/group-user/api/group-user-service.ts b/apps/st/src/entities/group-user/api/group-user-service.ts
new file mode 100644
index 000000000..85355a88f
--- /dev/null
+++ b/apps/st/src/entities/group-user/api/group-user-service.ts
@@ -0,0 +1,72 @@
+import { client } from "@/shared/api";
+import { ENDPOINT } from "@/shared/config";
+import type {
+ GroupUserService,
+ GroupUserSearchParams,
+ GroupUserListResponseDto,
+ GroupUserDetailResponse,
+ PageGroupUserList,
+ GroupUserSaveData,
+ GroupUserUpdateData,
+} from "../model/group-user-interface";
+
+export class GroupUsersApiService implements GroupUserService {
+ // 그룹 사용자 목록 조회
+ public async findPageGroupUser(
+ params: GroupUserSearchParams,
+ ): Promise {
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.GROUP_USERS}`,
+ { params },
+ );
+ return response.data;
+ }
+
+ // 그룹 사용자 단건 조회
+ public async findByGroupAndUserId(
+ grpSn: number,
+ userUnqId: string,
+ ): Promise {
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.GROUP_USERS}/${grpSn}/${userUnqId}`,
+ );
+ return response.data;
+ }
+
+ // 그룹 사용자 저장
+ public async saveGroupUser(
+ data: GroupUserSaveData[],
+ ): Promise {
+ const response = await client.post(
+ `${ENDPOINT.CMS_SERVICE.GROUP_USERS}`,
+ data,
+ );
+ return response.data;
+ }
+
+ // 그룹 사용자 수정
+ public async updateGroupUser(
+ grpSn: number,
+ userUnqId: string,
+ data: GroupUserUpdateData,
+ ): Promise {
+ const response = await client.put(
+ `${ENDPOINT.CMS_SERVICE.GROUP_USERS}/${grpSn}/${userUnqId}`,
+ data,
+ );
+ return response.data;
+ }
+
+ // 그룹 사용자 삭제
+ public async deleteGroupUser(
+ groupId: string,
+ userUnqId: string,
+ ): Promise {
+ const response = await client.delete(
+ `${ENDPOINT.CMS_SERVICE.GROUP_USERS}/${groupId}/${userUnqId}`,
+ );
+ return response.data;
+ }
+}
+
+export const GroupUsersApi = new GroupUsersApiService();
diff --git a/apps/st/src/entities/group-user/index.ts b/apps/st/src/entities/group-user/index.ts
new file mode 100644
index 000000000..349194a90
--- /dev/null
+++ b/apps/st/src/entities/group-user/index.ts
@@ -0,0 +1,5 @@
+export { GroupUsersApi } from "./api/group-user-service";
+export {
+ type GroupUserListResponseDto,
+ type GroupUserSaveData,
+} from "./model/group-user-interface";
diff --git a/apps/st/src/entities/group-user/lib/check-statistics-input-toolbar-group-membership.ts b/apps/st/src/entities/group-user/lib/check-statistics-input-toolbar-group-membership.ts
new file mode 100644
index 000000000..647442b2c
--- /dev/null
+++ b/apps/st/src/entities/group-user/lib/check-statistics-input-toolbar-group-membership.ts
@@ -0,0 +1,55 @@
+import { GroupUsersApi } from "../api/group-user-service";
+
+export const GRP_ST_INPUT = "GRP_ST_INPUT";
+export const GRP_ST_CONFIRM = "GRP_ST_CONFIRM";
+export const GRP_ADMIN = "GRP_ADMIN";
+
+const PAGE_SIZE = 500;
+
+/**
+ * 지정한 그룹(`keyword`)마다 `findPageGroupUser`를 호출하고,
+ * 응답 목록이 비어 있지 않으며 로그인 사용자 `userUnqId`와 일치하는 행이 있으면 true.
+ */
+export async function checkPageGroupUserMembershipForUser(
+ userName: string,
+ userUnqId: string,
+ groupKeywords: readonly string[],
+): Promise {
+ const uid = userUnqId.trim();
+ const nm = userName.trim();
+ if (!uid || !nm) return false;
+
+ const paramsBase = { userNm: nm, page: 0, size: PAGE_SIZE };
+
+ for (const keyword of groupKeywords) {
+ const page = await GroupUsersApi.findPageGroupUser({
+ ...paramsBase,
+ keyword,
+ });
+ const rows = page.content ?? [];
+ if (rows.length > 0 && rows.some((r) => String(r.userUnqId ?? "").trim() === uid)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * 통계 입력 화면 상단 액션 — `GRP_ST_INPUT` / `GRP_ADMIN`.
+ */
+export async function checkStatisticsInputToolbarGroupMembership(
+ userName: string,
+ userUnqId: string,
+): Promise {
+ return checkPageGroupUserMembershipForUser(userName, userUnqId, [GRP_ST_INPUT, GRP_ADMIN]);
+}
+
+/**
+ * 통계 입력 현황 승인 등 — `GRP_ST_CONFIRM` / `GRP_ADMIN`.
+ */
+export async function checkStatisticsInputStatusConfirmGroupMembership(
+ userName: string,
+ userUnqId: string,
+): Promise {
+ return checkPageGroupUserMembershipForUser(userName, userUnqId, [GRP_ST_CONFIRM, GRP_ADMIN]);
+}
diff --git a/apps/st/src/entities/group-user/model/group-user-interface.ts b/apps/st/src/entities/group-user/model/group-user-interface.ts
new file mode 100644
index 000000000..52fa97fce
--- /dev/null
+++ b/apps/st/src/entities/group-user/model/group-user-interface.ts
@@ -0,0 +1,79 @@
+import type { PageableList, PageableParams } from "@/shared/model";
+
+// 그룹 사용자 검색 파라미터 인터페이스
+export interface GroupUserSearchParams extends PageableParams {
+ keyword?: string;
+ userNm?: string;
+}
+
+// 그룹 사용자 목록 응답 인터페이스
+export interface GroupUserListResponseDto {
+ mode: string;
+ groupId: string;
+ userUnqId: string;
+ userId: string;
+ userName: string;
+ ogdpSeCd: string;
+ ogdpSeNm: string;
+ mphn: string;
+}
+
+// 그룹 사용자 페이지 응답 인터페이스
+export type PageGroupUserList = PageableList;
+
+// 그룹 사용자 상세 응답 인터페이스
+export interface GroupUserDetailResponse {
+ grpSn: number;
+ userUnqId: string;
+ userId: string;
+ userName: string;
+ email: string;
+ mphn: string;
+ regDt: string;
+ rgtr: string;
+ mdfcnDt: string;
+ mdfr: string;
+ rgtrNm: string;
+ mdfrNm: string;
+}
+
+// 그룹 사용자 저장 데이터 인터페이스
+export interface GroupUserSaveData {
+ mode: string;
+ groupId: string;
+ userUnqId: string;
+}
+
+// 그룹 사용자 수정 데이터 인터페이스
+export interface GroupUserUpdateData {
+ userUnqId: string;
+}
+
+// 그룹 사용자 서비스 인터페이스
+export interface GroupUserService {
+ // 그룹 사용자 목록 조회
+ findPageGroupUser: (
+ params: GroupUserSearchParams,
+ ) => Promise;
+
+ // 그룹 사용자 단건 조회
+ findByGroupAndUserId: (
+ grpSn: number,
+ userUnqId: string,
+ ) => Promise;
+
+ // 그룹 사용자 저장
+ saveGroupUser: (
+ data: GroupUserSaveData[],
+ ) => Promise;
+
+ // 그룹 사용자 수정
+ updateGroupUser: (
+ grpSn: number,
+ userUnqId: string,
+ data: GroupUserUpdateData,
+ ) => Promise;
+
+ // 그룹 사용자 삭제
+ deleteGroupUser: (groupId: string, userUnqId: string) => Promise;
+}
diff --git a/apps/st/src/entities/statistics/api/statistics-service.ts b/apps/st/src/entities/statistics/api/statistics-service.ts
index 0fdfbf292..20edb9935 100644
--- a/apps/st/src/entities/statistics/api/statistics-service.ts
+++ b/apps/st/src/entities/statistics/api/statistics-service.ts
@@ -21,6 +21,8 @@ import {
type ReviewSubmitResponse,
type SearchCondition,
type Sgg,
+ type StatiscicsManagementReviewHistoryRequest,
+ type StatiscicsManagementReviewHistoryResponse,
type StatisticsColumnGuideListResponse,
type StatisticsLogDetailResponse,
type StatisticsLogRequest,
@@ -301,6 +303,14 @@ export class StatisticsApiService implements StatisticsService {
);
return response.data;
}
+
+ public async getStatiscicsManagementReviewHistory(param: StatiscicsManagementReviewHistoryRequest): Promise {
+ const response = await stClient.get(
+ `${ENDPOINT.ST_SERVICE.REVIEW}/stats-review`,
+ { params: param },
+ );
+ return response.data;
+ }
}
export const statisticsApi = new StatisticsApiService();
diff --git a/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts
index efda5e1aa..2c82cbb0d 100644
--- a/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts
+++ b/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts
@@ -187,10 +187,11 @@ export interface PublicSewageMainFacilityMappingSaveRequest {
admdstNm: string;
mainFcltSn: string;
mainFcltNm: string;
- prxyBzentyNmCn: string;
- cnsgnCstVl: number;
- wholNope: number;
- prxyCtrtCn: string;
+ /** 매핑 추가(saveList 있음) 시 입력. 매핑 삭제만 할 때는 요청에서 생략 가능 */
+ prxyBzentyNmCn?: string;
+ cnsgnCstVl?: number;
+ wholNope?: number;
+ prxyCtrtCn?: string;
userId: string;
saveList: PublicSewageMainFacilityMappingSaveItem[];
deleteList: string[];
diff --git a/apps/st/src/entities/statistics/model/statistics-interface.ts b/apps/st/src/entities/statistics/model/statistics-interface.ts
index cd8caaf77..de3a9bf7b 100644
--- a/apps/st/src/entities/statistics/model/statistics-interface.ts
+++ b/apps/st/src/entities/statistics/model/statistics-interface.ts
@@ -393,8 +393,10 @@ export interface StatsInputStatusApproveBatchRequest {
/** 통계 입력 현황 일괄 승인 API 응답 */
export interface StatsInputStatusApproveResponse {
- status: string;
- message?: string;
+ exceptionCnt: number;
+ message: string;
+ resultCode: string;
+ updatedStts: string;
}
export interface SubmitVerifyExceptionItem {
@@ -434,6 +436,32 @@ export interface ReviewerInfoResponse {
}
}
+export interface StatiscicsManagementReviewHistoryRequest {
+ statsYr: string;
+ sewerBzmnCd?: string;
+ ctpvNm?: string;
+ statsCd?: string;
+}
+
+/** 통계관리 검토 이력 조회 API `data` 배열 요소 */
+export interface StatiscicsManagementReviewHistoryItem {
+ statsYr: string;
+ ctpvNm: string;
+ sggNm: string;
+ sewerBzmnCd: string;
+ statsCd: string;
+ menuNm: string;
+ wrterOpinionSm: string | null;
+ chckerOpinionSm: string | null;
+ returnOpinionSm: string | null;
+}
+
+export interface StatiscicsManagementReviewHistoryResponse {
+ status: string;
+ message?: string;
+ data: StatiscicsManagementReviewHistoryItem[];
+}
+
export interface StatisticsService {
getSidoList(params?: { instSeCd?: string }): Promise>;
getSggList(sidoName: string, params?: { instSeCd?: string; gugunSkipYn?: "Y" | "N" }): Promise>;
@@ -461,4 +489,5 @@ export interface StatisticsService {
SubmitVerifyException(param: SubmitVerifyExceptionRequest): Promise;
getColumnGuides(param: string): Promise;
getReviewerInfo(param: ReviewerInfoRequest): Promise;
+ getStatiscicsManagementReviewHistory(param: StatiscicsManagementReviewHistoryRequest): Promise;
}
\ No newline at end of file
diff --git a/apps/st/src/entities/statistics/model/statistics-verification-report/statistics-verification-report-columns.ts b/apps/st/src/entities/statistics/model/statistics-verification-report/statistics-verification-report-columns.ts
index 969d3c6b3..025654b05 100644
--- a/apps/st/src/entities/statistics/model/statistics-verification-report/statistics-verification-report-columns.ts
+++ b/apps/st/src/entities/statistics/model/statistics-verification-report/statistics-verification-report-columns.ts
@@ -26,14 +26,6 @@ export const statisticsVerificationReportColumnInfo: ColumnInfo = {
styleName: READ_ONLY_STYLE,
editable: false,
},
- {
- name: "locgStatusNm",
- fieldName: "locgStatusNm",
- header: { text: "진행상태" },
- width: 120,
- styleName: READ_ONLY_STYLE,
- editable: false,
- },
{
name: "laePicNm",
fieldName: "laePicNm",
@@ -109,7 +101,6 @@ export const statisticsVerificationReportColumnInfo: ColumnInfo = {
"ctpvNm",
"sggNm",
"menuTabName",
- "locgStatusNm",
"laePicNm",
"laePicTelNo",
"laeKbrdrDeptNm",
diff --git a/apps/st/src/pages/statistics/ui/panels/vrfc-report-panel.tsx b/apps/st/src/pages/statistics/ui/panels/vrfc-report-panel.tsx
index 51834d857..1463cc035 100644
--- a/apps/st/src/pages/statistics/ui/panels/vrfc-report-panel.tsx
+++ b/apps/st/src/pages/statistics/ui/panels/vrfc-report-panel.tsx
@@ -17,11 +17,13 @@ interface VrfcReportPanelProps {
const TYPE_CODE_NAME_MAP: Record = {
NV: "일반",
IMR: "I-MR",
+ HB: "HB",
};
const TYPE_CODE_COLOR_MAP: Record = {
NV: "gray",
IMR: "red",
+ HB: "blue",
};
export function VrfcReportPanel({ params }: VrfcReportPanelProps) {
diff --git a/apps/st/src/shared/lib/index.ts b/apps/st/src/shared/lib/index.ts
index e2ddab13d..ec5104865 100644
--- a/apps/st/src/shared/lib/index.ts
+++ b/apps/st/src/shared/lib/index.ts
@@ -5,5 +5,9 @@ export { useDataTable } from "./use-data-table";
export { useGlobalProgressStore } from "./use-global-progress-store";
export { useMenuStore } from "./use-menu-store";
export { useUserStore } from "./use-user-store";
+export {
+ useStatisticsInputStatusConfirmGroupGate,
+ useStatisticsInputToolbarGroupGate,
+} from "./use-statistics-input-toolbar-group-gate";
export { usePrompt } from "./use-prompt";
export { useSelectPrompt } from "./use-select-prompt";
diff --git a/apps/st/src/shared/lib/use-statistics-input-toolbar-group-gate.ts b/apps/st/src/shared/lib/use-statistics-input-toolbar-group-gate.ts
new file mode 100644
index 000000000..e8337f496
--- /dev/null
+++ b/apps/st/src/shared/lib/use-statistics-input-toolbar-group-gate.ts
@@ -0,0 +1,68 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+import {
+ checkStatisticsInputStatusConfirmGroupMembership,
+ checkStatisticsInputToolbarGroupMembership,
+} from "@/entities/group-user/lib/check-statistics-input-toolbar-group-membership";
+
+import { useUserStore } from "./use-user-store";
+
+/** 통계 입력 상단 툴바(저장·제출·검증결과 등) — 그룹 멤버십 만족 시에만 true */
+export function useStatisticsInputToolbarGroupGate(): boolean {
+ const userInfo = useUserStore((s) => s.userInfo);
+ const [allowed, setAllowed] = useState(false);
+
+ useEffect(() => {
+ let cancelled = false;
+ const name = userInfo?.userName?.trim() ?? "";
+ const uid = userInfo?.userUnqId?.trim() ?? "";
+ if (!name || !uid) {
+ setAllowed(false);
+ return;
+ }
+ void (async () => {
+ try {
+ const ok = await checkStatisticsInputToolbarGroupMembership(name, uid);
+ if (!cancelled) setAllowed(ok);
+ } catch {
+ if (!cancelled) setAllowed(false);
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [userInfo?.userName, userInfo?.userUnqId]);
+
+ return allowed;
+}
+
+/** 통계 입력 현황 — `GRP_ST_CONFIRM` / `GRP_ADMIN` 멤버일 때만 true */
+export function useStatisticsInputStatusConfirmGroupGate(): boolean {
+ const userInfo = useUserStore((s) => s.userInfo);
+ const [allowed, setAllowed] = useState(false);
+
+ useEffect(() => {
+ let cancelled = false;
+ const name = userInfo?.userName?.trim() ?? "";
+ const uid = userInfo?.userUnqId?.trim() ?? "";
+ if (!name || !uid) {
+ setAllowed(false);
+ return;
+ }
+ void (async () => {
+ try {
+ const ok = await checkStatisticsInputStatusConfirmGroupMembership(name, uid);
+ if (!cancelled) setAllowed(ok);
+ } catch {
+ if (!cancelled) setAllowed(false);
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [userInfo?.userName, userInfo?.userUnqId]);
+
+ return allowed;
+}
|