Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/as/dev

This commit is contained in:
2026-07-07 17:51:28 +09:00
470 changed files with 44928 additions and 7383 deletions
-6
View File
@@ -1,6 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+15 -11
View File
@@ -1,17 +1,29 @@
"use client";
import { useState } from "react";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import {
getCoreRowModel,
type PaginationState,
type Row,
type RowSelectionState,
type Table,
type Row,
} from "@tanstack/react-table";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useDataTable } from "@/shared/lib";
import { GroupApi } from "@/entities/group";
import { GroupUsersApi } from "@/entities/group-user";
import {
type AdminUserListResponse,
UserApi,
type UserSearchParams,
} from "@/entities/user";
import {
Button,
Checkbox,
Input,
Label,
Select,
@@ -19,18 +31,10 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
Checkbox,
} from "@common/components";
import { GroupApi } from "@/entities/group";
import {
UserApi,
type UserSearchParams,
type AdminUserListResponse,
} from "@/entities/user";
import { GroupUsersApi } from "@/entities/group-user";
import { ENDPOINT } from "@/shared/config";
import { useDataTable, useAlertStore } from "@/shared/lib";
import { columns } from "../model/table-columns";
import { AddAdminDialog } from "./add-admin-dialog";
function AdminPage() {
+41 -34
View File
@@ -1,20 +1,25 @@
"use client";
import { useEffect, useState, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import {
getCoreRowModel,
type OnChangeFn,
type PaginationState,
type RowSelectionState,
} from "@tanstack/react-table";
import { Button, Input, Label } from "@common/components";
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
import { ENDPOINT } from "@/shared/config";
import { useDataTable } from "@/shared/lib";
import { useDataTable, useSearchParamsState } from "@/shared/lib";
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
import { Button, Input, Label } from "@common/components";
import { BannerApi } from "../api/banner-service";
import { type Banner } from "../model/banner-interface";
import { createColumns } from "../model/banner-table-columns";
import { type Banner, type BannerParams } from "../model/banner-interface";
import { PreviewBannerDialog } from "./preview-banner-dialog";
type PostingTarget = {
@@ -22,50 +27,52 @@ type PostingTarget = {
pstgTrgtNo: number;
};
const DEFAULT_PARAMS = {
bnrTtl: "",
sort: "regDt,desc",
page: 0,
size: 10,
};
function BannerPage() {
const router = useRouter();
const imageRefs = useRef<Map<number, string>>(new Map());
const { params, setParams, resetParams } =
useSearchParamsState(DEFAULT_PARAMS);
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
const [bnrTtl, setBnrTtl] = useState("");
const [searchParams, setSearchParams] = useState<BannerParams>({
bnrTtl: "",
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [bnrTtl, setBnrTtl] = useState(params.bnrTtl);
const query = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.BANNERS, searchParams, pagination],
queryKey: [ENDPOINT.CMS_SERVICE.BANNERS, params],
queryFn: () =>
BannerApi.getBannerList({
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
bnrTtl: params.bnrTtl,
sort: params.sort,
page: params.page,
size: params.size,
}),
placeholderData: keepPreviousData,
});
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
const pagination: PaginationState = {
pageIndex: params.page,
pageSize: params.size,
};
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
const next = typeof updater === "function" ? updater(pagination) : updater;
setParams({ page: next.pageIndex, size: next.pageSize });
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
bnrTtl,
});
setParams({ bnrTtl, page: 0 });
};
const handleReset = () => {
setBnrTtl("");
setSearchParams({
...searchParams,
bnrTtl: "",
});
resetParams();
};
const handleRowClick = (row: Banner) => {
@@ -92,7 +99,7 @@ function BannerPage() {
manualPagination: true,
enableMultiRowSelection: true,
getCoreRowModel: getCoreRowModel(),
onPaginationChange: setPagination,
onPaginationChange: handlePaginationChange,
onRowSelectionChange: setRowSelection,
},
dataTableOptions: {
+42 -38
View File
@@ -2,12 +2,19 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import {
getCoreRowModel,
type OnChangeFn,
type PaginationState,
type RowSelectionState,
} from "@tanstack/react-table";
import { ENDPOINT } from "@/shared/config";
import { useDataTable, useSearchParamsState } from "@/shared/lib";
import { type Board, BoardApi } from "@/entities/board";
import { CodeSelect } from "@/entities/code";
import {
Button,
Input,
@@ -18,60 +25,57 @@ import {
SelectTrigger,
SelectValue,
} from "@common/components";
import { CodeSelect } from "@/entities/code";
import { BoardApi, type BoardParams } from "@/entities/board";
import { ENDPOINT } from "@/shared/config";
import { useDataTable } from "@/shared/lib";
import { type Board } from "@/entities/board";
import { columns } from "../model/table-columns";
const DEFAULT_PARAMS = {
bbsTypeCd: "",
bbsNm: "",
sort: "regDt,desc",
page: 0,
size: 10,
};
function BbsPage() {
const router = useRouter();
const { params, setParams, resetParams } =
useSearchParamsState(DEFAULT_PARAMS);
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [bbsTypeCd, setBbsTypeCd] = useState("");
const [bbsNm, setBbsNm] = useState("");
const [searchParams, setSearchParams] = useState<BoardParams>({
bbsTypeCd: "",
bbsNm: "",
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [bbsTypeCd, setBbsTypeCd] = useState(params.bbsTypeCd);
const [bbsNm, setBbsNm] = useState(params.bbsNm);
const query = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.BBS, searchParams, pagination],
queryKey: [ENDPOINT.BOARD_SERVICE.BBS, params],
queryFn: () =>
BoardApi.getBoardList({
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
bbsTypeCd: params.bbsTypeCd,
bbsNm: params.bbsNm,
sort: params.sort,
page: params.page,
size: params.size,
}),
placeholderData: keepPreviousData,
});
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
const pagination: PaginationState = {
pageIndex: params.page,
pageSize: params.size,
};
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
const next = typeof updater === "function" ? updater(pagination) : updater;
setParams({ page: next.pageIndex, size: next.pageSize });
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
bbsTypeCd,
bbsNm,
});
setParams({ bbsTypeCd, bbsNm, page: 0 });
};
const handleReset = () => {
setBbsTypeCd("");
setBbsNm("");
setSearchParams({
...searchParams,
bbsTypeCd: "",
bbsNm: "",
});
resetParams();
};
const handleRowClick = (row: Board) => {
@@ -89,7 +93,7 @@ function BbsPage() {
manualPagination: true,
enableMultiRowSelection: true,
getCoreRowModel: getCoreRowModel(),
onPaginationChange: setPagination,
onPaginationChange: handlePaginationChange,
onRowSelectionChange: setRowSelection,
},
dataTableOptions: {
@@ -1,8 +1,11 @@
import dayjs from "dayjs";
import { useState } from "react";
import type { ColumnDef } from "@tanstack/react-table";
import { Button } from "@common/components";
import dayjs from "dayjs";
import type { UpCode } from "@/entities/code";
import { Button } from "@common/components";
import { UpCodeManagementDialog } from "../ui/upcode-dialog";
export const columns: ColumnDef<UpCode>[] = [
@@ -24,7 +27,7 @@ export const columns: ColumnDef<UpCode>[] = [
header: "상위코드명",
},
{
accessorKey: "cdSe",
accessorKey: "cdSeNm",
header: "코드구분",
},
{
+39 -35
View File
@@ -2,60 +2,64 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
import { Button, Input, Label } from "@common/components";
import {
ContentsApi,
type Contents,
type ContentsParams,
} from "@/entities/content";
getCoreRowModel,
type OnChangeFn,
type PaginationState,
} from "@tanstack/react-table";
import { ENDPOINT } from "@/shared/config";
import { useDataTable } from "@/shared/lib";
import { useDataTable, useSearchParamsState } from "@/shared/lib";
import { type Contents, ContentsApi } from "@/entities/content";
import { Button, Input, Label } from "@common/components";
import { columns } from "../model/table-columns";
const DEFAULT_PARAMS = {
contsTtl: "",
sort: "regDt,desc",
page: 0,
size: 10,
};
function ContentsPage() {
const router = useRouter();
const [contsTtl, setContsTtl] = useState("");
const [searchParams, setSearchParams] = useState<ContentsParams>({
contsTtl: "",
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const { params, setParams, resetParams } =
useSearchParamsState(DEFAULT_PARAMS);
const [contsTtl, setContsTtl] = useState(params.contsTtl);
const query = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS, searchParams, pagination],
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS, params],
queryFn: () =>
ContentsApi.getContentsList({
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
contsTtl: params.contsTtl,
sort: params.sort,
page: params.page,
size: params.size,
}),
placeholderData: keepPreviousData,
});
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
const pagination: PaginationState = {
pageIndex: params.page,
pageSize: params.size,
};
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
const next = typeof updater === "function" ? updater(pagination) : updater;
setParams({ page: next.pageIndex, size: next.pageSize });
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
contsTtl,
});
setParams({ contsTtl, page: 0 });
};
const handleReset = () => {
setContsTtl("");
setSearchParams({
...searchParams,
contsTtl: "",
});
resetParams();
};
const handleRowClick = (row: Contents) => {
@@ -69,7 +73,7 @@ function ContentsPage() {
state: {
pagination,
},
onPaginationChange: setPagination,
onPaginationChange: handlePaginationChange,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
},
@@ -354,7 +354,7 @@ function DashboardPage() {
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">실시간 알림</h3>
</div>
<div className="p-4">
<div className="p-4" style={{ height: "120px", overflowY: "auto" }}>
<div className="space-y-4 text-foreground">
{noticeUserList?.map((item) => (
<div
@@ -373,7 +373,7 @@ function DashboardPage() {
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">일정 알림</h3>
</div>
<div className="p-4">
<div className="p-4" style={{ height: "120px", overflowY: "auto" }}>
<div className="space-y-4 text-foreground">
{scheduleList && scheduleList.length > 0 ? (
scheduleList.map((schedule, index) => {
+7 -4
View File
@@ -1,13 +1,16 @@
"use client";
import { useState } from "react";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { Button, Input, Label } from "@common/components";
import { AttachmentApi, type AttachmentParams } from "@/entities/attachment";
import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib";
import { AttachmentApi, type AttachmentParams } from "@/entities/attachment";
import { Button, Input, Label } from "@common/components";
import { createColumns } from "../model/table-columns";
function FilePage() {
+31 -13
View File
@@ -23,6 +23,11 @@ import {
Input,
Checkbox,
Label,
Select,
SelectValue,
SelectContent,
SelectItem,
SelectTrigger,
} from "@common/components";
import {
UserApi,
@@ -42,9 +47,11 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
const alert = useAlertStore((state) => state.setMessage);
const [open, setOpen] = useState(false);
const [userName, setUserName] = useState("");
const [keywordType, setKeywordType] = useState("all");
const [keyword, setKeyword] = useState("");
const [searchParam, setSearchParam] = useState<UserSearchParams>({
userName: "",
keywordType: "all",
keyword: "",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
@@ -156,7 +163,7 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
...pagination,
pageIndex: 0,
});
setSearchParam({ userName });
setSearchParam({ keywordType, keyword });
};
const resetSearch = () => {
@@ -164,8 +171,8 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
pageIndex: 0,
pageSize: 10,
});
setUserName("");
setSearchParam({ userName: "" });
setKeyword("");
setSearchParam({ keywordType: "all", keyword: "" });
setUserRowSelection({});
};
@@ -184,15 +191,26 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
</DialogHeader>
<form onSubmit={handleSearch} className="mb-4 flex items-end gap-2">
<div className="flex items-center gap-2">
<Label htmlFor="userName" className="mr-2 text-label">
사용자 이름
</Label>
<Select
value={keywordType}
onValueChange={(value) => setKeywordType(value)}
>
<SelectTrigger className="w-[18rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">전체</SelectItem>
<SelectItem value="userId">아이디</SelectItem>
<SelectItem value="userName">이름</SelectItem>
<SelectItem value="email">이메일</SelectItem>
<SelectItem value="mphn">휴대폰번호</SelectItem>
</SelectContent>
</Select>
<Input
id="userName"
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
value={userName}
onChange={(e) => setUserName(e.target.value)}
placeholder="사용자 이름으로 검색"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
className="!text-[1.3rem]"
placeholder="검색어를 입력하세요"
/>
</div>
<Button className="h-[3.2rem]" type="submit">
+37 -32
View File
@@ -2,68 +2,73 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import {
getCoreRowModel,
type OnChangeFn,
type PaginationState,
type RowSelectionState,
} from "@tanstack/react-table";
import { Button, Input, Label } from "@common/components";
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
import { ENDPOINT } from "@/shared/config";
import { useDataTable } from "@/shared/lib";
import { useDataTable, useSearchParamsState } from "@/shared/lib";
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
import { Button, Input, Label } from "@common/components";
import { ImageApi } from "../api/image-service";
import { type Image } from "../model/image-interface";
import { columns } from "../model/image-table-columns";
import { type Image, type ImageParams } from "../model/image-interface";
type PostingTarget = {
pstgTrgtNm: string;
pstgTrgtNo: number;
};
const DEFAULT_PARAMS = {
keyword: "",
sort: "regDt,desc",
page: 0,
size: 10,
};
function ImagePage() {
const router = useRouter();
const [keyword, setKeyword] = useState("");
const [searchParams, setSearchParams] = useState<ImageParams>({
keyword: "",
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const { params, setParams, resetParams } =
useSearchParamsState(DEFAULT_PARAMS);
const [keyword, setKeyword] = useState(params.keyword);
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
const query = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.IMAGES, searchParams, pagination],
queryKey: [ENDPOINT.CMS_SERVICE.IMAGES, params],
queryFn: () =>
ImageApi.getImageList({
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
keyword: params.keyword,
sort: params.sort,
page: params.page,
size: params.size,
}),
});
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
const pagination: PaginationState = {
pageIndex: params.page,
pageSize: params.size,
};
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
const next = typeof updater === "function" ? updater(pagination) : updater;
setParams({ page: next.pageIndex, size: next.pageSize });
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
keyword,
});
setParams({ keyword, page: 0 });
};
const handleReset = () => {
setKeyword("");
setSearchParams({
...searchParams,
keyword: "",
});
resetParams();
};
const handleRowClick = (row: Image) => {
@@ -81,7 +86,7 @@ function ImagePage() {
manualPagination: true,
enableMultiRowSelection: true,
getCoreRowModel: getCoreRowModel(),
onPaginationChange: setPagination,
onPaginationChange: handlePaginationChange,
onRowSelectionChange: setRowSelection,
},
dataTableOptions: {
+55 -47
View File
@@ -2,45 +2,76 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
import { Button, Input, Label } from "@common/components";
import { CodeSelect } from "@/entities/code";
import { ENDPOINT } from "@/shared/config";
import { useDataTable } from "@/shared/lib";
import {
AuthorizationsApi,
getCoreRowModel,
type OnChangeFn,
type PaginationState,
} from "@tanstack/react-table";
import { ENDPOINT } from "@/shared/config";
import { useDataTable, useSearchParamsState } from "@/shared/lib";
import {
type Authorizations,
type AuthorizationsParams,
AuthorizationsApi,
} from "@/entities/authorization";
import { CodeSelect } from "@/entities/code";
import { Button, Input, Label } from "@common/components";
import { columns } from "../model/authorizations-table-columns";
const DEFAULT_PARAMS = {
authorizationName: "",
httpMethodCode: "",
sort: "regDt,desc",
page: 0,
size: 10,
};
function ModulePage() {
const router = useRouter();
const [authorizationName, setAuthorizationName] = useState("");
const [httpMethodCode, setHttpMethodCode] = useState("");
const [searchParams, setSearchParams] = useState<AuthorizationsParams>({
authorizationName: "",
httpMethodCode: "",
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const { params, setParams, resetParams } =
useSearchParamsState(DEFAULT_PARAMS);
const [authorizationName, setAuthorizationName] = useState(
params.authorizationName,
);
const [httpMethodCode, setHttpMethodCode] = useState(params.httpMethodCode);
const query = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.AUTHORIZATIONS, searchParams, pagination],
queryKey: [ENDPOINT.USER_SERVICE.AUTHORIZATIONS, params],
queryFn: () =>
AuthorizationsApi.getAuthorizationsList({
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
authorizationName: params.authorizationName,
httpMethodCode: params.httpMethodCode,
sort: params.sort,
page: params.page,
size: params.size,
}),
placeholderData: keepPreviousData,
});
const pagination: PaginationState = {
pageIndex: params.page,
pageSize: params.size,
};
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
const next = typeof updater === "function" ? updater(pagination) : updater;
setParams({ page: next.pageIndex, size: next.pageSize });
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setParams({ authorizationName, httpMethodCode, page: 0 });
};
const handleReset = () => {
setAuthorizationName("");
setHttpMethodCode("");
resetParams();
};
const handleRowClick = (row: Authorizations) => {
router.push(`/module/register/${row.authorizationNo}`);
};
@@ -52,7 +83,7 @@ function ModulePage() {
state: {
pagination,
},
onPaginationChange: setPagination,
onPaginationChange: handlePaginationChange,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
},
@@ -62,29 +93,6 @@ function ModulePage() {
},
});
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
e.preventDefault();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
authorizationName,
httpMethodCode,
});
};
const handleReset = () => {
setAuthorizationName("");
setHttpMethodCode("");
setSearchParams({
...searchParams,
authorizationName: "",
httpMethodCode: "",
});
};
return (
<div className="card card-border !mt-6">
<div className="card card-border">
+37 -32
View File
@@ -2,70 +2,75 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import {
getCoreRowModel,
type OnChangeFn,
type PaginationState,
type RowSelectionState,
} from "@tanstack/react-table";
import { Button, Input, Label } from "@common/components";
import { ENDPOINT } from "@/shared/config";
import { useDataTable, useSearchParamsState } from "@/shared/lib";
import { AuthorityButton } from "@/entities/menu-authority";
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
import { ENDPOINT } from "@/shared/config";
import { useDataTable } from "@/shared/lib";
import { Button, Input, Label } from "@common/components";
import { PopupApi } from "../api/popup-service";
import type { Popup } from "../model/popup-interface";
import { columns } from "../model/table-columns";
import type { Popup, PopupParams } from "../model/popup-interface";
type PostingTarget = {
pstgTrgtNm: string;
pstgTrgtNo: number;
};
const DEFAULT_PARAMS = {
keyword: "",
sort: "regDt,desc",
page: 0,
size: 10,
};
function PopupPage() {
const router = useRouter();
const [keyword, setKeyword] = useState("");
const [searchParams, setSearchParams] = useState<PopupParams>({
keyword: "",
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const { params, setParams, resetParams } =
useSearchParamsState(DEFAULT_PARAMS);
const [keyword, setKeyword] = useState(params.keyword);
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
const query = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.POPUPS, searchParams, pagination],
queryKey: [ENDPOINT.CMS_SERVICE.POPUPS, params],
queryFn: () =>
PopupApi.getPopupList({
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
keyword: params.keyword,
sort: params.sort,
page: params.page,
size: params.size,
}),
placeholderData: keepPreviousData,
});
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
const pagination: PaginationState = {
pageIndex: params.page,
pageSize: params.size,
};
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
const next = typeof updater === "function" ? updater(pagination) : updater;
setParams({ page: next.pageIndex, size: next.pageSize });
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
keyword,
});
setParams({ keyword, page: 0 });
};
const handleReset = () => {
setKeyword("");
setSearchParams({
...searchParams,
keyword: "",
});
resetParams();
};
const handleRowClick = (row: Popup) => {
@@ -82,7 +87,7 @@ function PopupPage() {
},
manualPagination: true,
getCoreRowModel: getCoreRowModel(),
onPaginationChange: setPagination,
onPaginationChange: handlePaginationChange,
onRowSelectionChange: setRowSelection,
},
dataTableOptions: {
+98 -82
View File
@@ -1,15 +1,35 @@
"use client";
import dayjs from "dayjs";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import {
getCoreRowModel,
type OnChangeFn,
type PaginationState,
type RowSelectionState,
} from "@tanstack/react-table";
import dayjs from "dayjs";
import { ENDPOINT } from "@/shared/config";
import {
useAlertStore,
useDataTable,
useSearchParamsState,
useSelectPrompt,
} from "@/shared/lib";
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
import { BoardApi } from "@/entities/board";
import { AuthorityButton } from "@/entities/menu-authority";
import {
type DeletePost,
deleteReasonMessages,
PostApi,
type RecentPost,
} from "@/entities/post";
import { SortSelect } from "@/widgets/post";
import {
Button,
Input,
@@ -20,76 +40,72 @@ import {
SelectTrigger,
SelectValue,
} from "@common/components";
import { SortSelect } from "@/widgets/post";
import {
PostApi,
deleteReasonMessages,
type DeletePost,
type RecentPost,
type RecentPostParams,
} from "@/entities/post";
import { BoardApi } from "@/entities/board";
import { AuthorityButton } from "@/entities/menu-authority";
import { ENDPOINT } from "@/shared/config";
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
import { useAlertStore, useSelectPrompt, useDataTable } from "@/shared/lib";
import { columns } from "../model/table-columns";
import { MovePostDialog } from "./move-post-dialog";
type DateRange = {
from?: Date | undefined;
to?: Date | undefined;
from?: Date | null | undefined;
to?: Date | null | undefined;
};
const selectData = Object.entries(deleteReasonMessages).map(
([label, value]) => ({ label, value }),
);
const DEFAULT_PARAMS = {
keywordType: "",
keyword: "",
bbsId: "",
delYn: "N",
frDateYmd: "",
toDateYmd: "",
sort: "regDt,desc",
page: 0,
size: 10,
};
function parseDateRange(frDateYmd: string, toDateYmd: string): DateRange {
return {
from: frDateYmd ? dayjs(frDateYmd, "YYYYMMDD").toDate() : undefined,
to: toDateYmd ? dayjs(toDateYmd, "YYYYMMDD").toDate() : undefined,
};
}
function RecentPage() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const { prompt } = useSelectPrompt(selectData);
const { confirm } = useConfirm();
const alert = useAlertStore((state) => state.setMessage);
const { params, setParams, resetParams } =
useSearchParamsState(DEFAULT_PARAMS);
const [tableSort, setTableSort] = useState("regDt,desc");
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [presetDateRange, setPresetDateRange] = useState("");
const [dateRange, setDateRange] = useState<DateRange>({
from: undefined,
to: undefined,
});
const [keywordType, setKeywordType] = useState("");
const [keyword, setKeyword] = useState("");
const [bbsId, setBbsId] = useState<string | null>(null);
const [delYn, setDelYn] = useState("N");
const [searchParams, setSearchParams] = useState<RecentPostParams>({
keywordType: "",
keyword: "",
bbsId: null,
delYn: "N",
frDateYmd: "",
toDateYmd: "",
sort: "regDt,desc",
});
const [isDeletePostData, setIsDeletePostData] = useState(false);
const [dateRange, setDateRange] = useState<DateRange>(() =>
parseDateRange(params.frDateYmd, params.toDateYmd),
);
const [keywordType, setKeywordType] = useState(params.keywordType);
const [keyword, setKeyword] = useState(params.keyword);
const [bbsId, setBbsId] = useState(params.bbsId);
const [delYn, setDelYn] = useState(params.delYn);
const query = useQuery({
queryKey: [
ENDPOINT.BOARD_SERVICE.POSTS,
searchParams,
pagination,
tableSort,
],
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, params],
queryFn: () =>
PostApi.getRecentPostList({
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
sort: tableSort,
keywordType: params.keywordType,
keyword: params.keyword,
bbsId: params.bbsId ? parseInt(params.bbsId) : null,
delYn: params.delYn,
frDateYmd: params.frDateYmd,
toDateYmd: params.toDateYmd,
sort: params.sort,
page: params.page,
size: params.size,
}),
placeholderData: keepPreviousData,
});
@@ -99,9 +115,22 @@ function RecentPage() {
queryFn: () => BoardApi.getBoardList(),
});
const pagination: PaginationState = {
pageIndex: params.page,
pageSize: params.size,
};
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
const next = typeof updater === "function" ? updater(pagination) : updater;
setParams({ page: next.pageIndex, size: next.pageSize });
};
const isDeletePostData = params.delYn === "Y";
const handleRowClick = (row: RecentPost) => {
const from = `${pathname}${searchParams?.toString() ? `?${searchParams.toString()}` : ""}`;
router.push(
`/bbs/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}`,
`/bbs/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}?from=${encodeURIComponent(from)}`,
);
};
@@ -116,7 +145,7 @@ function RecentPage() {
manualPagination: true,
enableMultiRowSelection: true,
getCoreRowModel: getCoreRowModel(),
onPaginationChange: setPagination,
onPaginationChange: handlePaginationChange,
onRowSelectionChange: setRowSelection,
},
dataTableOptions: {
@@ -127,45 +156,32 @@ function RecentPage() {
const isSelectedRow = table.getFilteredSelectedRowModel().rows.length > 0;
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsDeletePostData(delYn === "Y");
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
setParams({
keywordType,
keyword,
bbsId: bbsId ? parseInt(bbsId) : null,
bbsId,
delYn,
frDateYmd: dateRange?.from
? dayjs(dateRange.from).format("YYYYMMDD")
: "",
toDateYmd: dateRange?.to ? dayjs(dateRange.to).format("YYYYMMDD") : "",
page: 0,
});
};
const handleReset = () => {
setKeywordType("");
setKeyword("");
setBbsId(null);
setBbsId("");
setDelYn("N");
setPresetDateRange("");
setDateRange({
from: undefined,
to: undefined,
});
setSearchParams({
...searchParams,
keywordType: "",
keyword: "",
bbsId: null,
delYn: "N",
frDateYmd: "",
toDateYmd: "",
});
setIsDeletePostData(false);
resetParams();
};
const handlePresetDateRange = (value: string) => {
@@ -273,10 +289,7 @@ function RecentPage() {
setDateRange={handleDateRangeChange}
/>
<Label className="ml-4 mr-2 text-label">게시판</Label>
<Select
value={bbsId?.toString() || ""}
onValueChange={(value) => setBbsId(value ? value : null)}
>
<Select value={bbsId} onValueChange={setBbsId}>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
@@ -289,7 +302,7 @@ function RecentPage() {
</SelectContent>
</Select>
<Label className="ml-4 mr-2 text-label">상태</Label>
<Select value={delYn} onValueChange={(value) => setDelYn(value)}>
<Select value={delYn} onValueChange={setDelYn}>
<SelectTrigger className="w-[10rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
@@ -376,10 +389,10 @@ function RecentPage() {
</>
)}
<Select
value={table.getState().pagination.pageSize.toString()}
onValueChange={(value) => {
table.setPageSize(parseInt(value));
}}
value={params.size.toString()}
onValueChange={(value) =>
setParams({ size: parseInt(value), page: 0 })
}
>
<SelectTrigger className="h-[3.2rem] w-[12rem]">
<SelectValue placeholder="페이지 크기" />
@@ -392,7 +405,10 @@ function RecentPage() {
))}
</SelectContent>
</Select>
<SortSelect value={tableSort} onValueChange={setTableSort} />
<SortSelect
value={params.sort}
onValueChange={(value) => setParams({ sort: value })}
/>
</div>
</div>
</div>
+51 -42
View File
@@ -2,39 +2,68 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
import { Button, Input, Label } from "@common/components";
import { RoleApi, type Role, type RoleParams } from "@/entities/role";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import {
getCoreRowModel,
type OnChangeFn,
type PaginationState,
} from "@tanstack/react-table";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useDataTable } from "@/shared/lib";
import { useDataTable, useSearchParamsState } from "@/shared/lib";
import { type Role, RoleApi } from "@/entities/role";
import { Button, Input, Label } from "@common/components";
import { createColumns } from "../model/table-columns";
const DEFAULT_PARAMS = {
keywordType: "roleName",
keyword: "",
sort: "regDt,desc",
page: 0,
size: 10,
};
function RolePage() {
const router = useRouter();
const alert = useAlertStore((state) => state.setMessage);
const [keyword, setKeyword] = useState("");
const [searchParams, setSearchParams] = useState<RoleParams>({
keywordType: "roleName",
keyword: "",
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const { params, setParams, resetParams } =
useSearchParamsState(DEFAULT_PARAMS);
const [keyword, setKeyword] = useState(params.keyword);
const query = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.ROLES, searchParams, pagination],
queryKey: [ENDPOINT.USER_SERVICE.ROLES, params],
queryFn: () =>
RoleApi.getRoles({
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
keywordType: params.keywordType,
keyword: params.keyword,
sort: params.sort,
page: params.page,
size: params.size,
}),
placeholderData: keepPreviousData,
});
const pagination: PaginationState = {
pageIndex: params.page,
pageSize: params.size,
};
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
const next = typeof updater === "function" ? updater(pagination) : updater;
setParams({ page: next.pageIndex, size: next.pageSize });
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setParams({ keyword, page: 0 });
};
const handleReset = () => {
setKeyword("");
resetParams();
};
const handleRowClick = (row: Role) => {
router.push(`/role/register/${row.roleId}`);
};
@@ -48,7 +77,7 @@ function RolePage() {
},
manualPagination: true,
getCoreRowModel: getCoreRowModel(),
onPaginationChange: setPagination,
onPaginationChange: handlePaginationChange,
},
dataTableOptions: {
onRowClick: handleRowClick,
@@ -56,26 +85,6 @@ function RolePage() {
},
});
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
e.preventDefault();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
keyword,
});
};
const handleReset = () => {
setKeyword("");
setSearchParams({
...searchParams,
keyword: "",
});
};
return (
<div className="card card-border !mt-6">
<div className="card card-border">
+40 -29
View File
@@ -1,54 +1,65 @@
"use client";
import "@common/assets/styles/grid.css";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
import { Button, Input, Label } from "@common/components";
import { SiteApi, type Site, type SiteParams } from "@/entities/site";
import {
getCoreRowModel,
type OnChangeFn,
type PaginationState,
} from "@tanstack/react-table";
import { ENDPOINT } from "@/shared/config";
import { useDataTable } from "@/shared/lib";
import { useDataTable, useSearchParamsState } from "@/shared/lib";
import { type Site, SiteApi } from "@/entities/site";
import { Button, Input, Label } from "@common/components";
import { columns } from "../model/table-columns";
const DEFAULT_PARAMS = {
siteNm: "",
sort: "regDt,desc",
page: 0,
size: 10,
};
function SitePage() {
const router = useRouter();
const [siteNm, setSiteNm] = useState("");
const [searchParams, setSearchParams] = useState<SiteParams>({
siteNm: "",
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const { params, setParams, resetParams } =
useSearchParamsState(DEFAULT_PARAMS);
const [siteNm, setSiteNm] = useState(params.siteNm);
const query = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.SITES, searchParams, pagination],
queryKey: [ENDPOINT.CMS_SERVICE.SITES, params],
queryFn: () =>
SiteApi.getSiteList({
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
siteNm: params.siteNm,
sort: params.sort,
page: params.page,
size: params.size,
}),
placeholderData: keepPreviousData,
});
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
const pagination: PaginationState = {
pageIndex: params.page,
pageSize: params.size,
};
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
const next = typeof updater === "function" ? updater(pagination) : updater;
setParams({ page: next.pageIndex, size: next.pageSize });
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setSearchParams({
...searchParams,
siteNm,
});
setParams({ siteNm, page: 0 });
};
const handleReset = () => {
setSiteNm("");
setSearchParams({
...searchParams,
siteNm: "",
});
resetParams();
};
const handleRowClick = (row: Site) => {
@@ -62,7 +73,7 @@ function SitePage() {
state: {
pagination,
},
onPaginationChange: setPagination,
onPaginationChange: handlePaginationChange,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
},
+45 -40
View File
@@ -2,19 +2,23 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import {
getCoreRowModel,
type OnChangeFn,
type PaginationState,
type RowSelectionState,
} from "@tanstack/react-table";
import { Button, Input, Label } from "@common/components";
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
import { AuthorityButton } from "@/entities/menu-authority";
import { ENDPOINT } from "@/shared/config";
import { useDataTable } from "@/shared/lib";
import { useDataTable, useSearchParamsState } from "@/shared/lib";
import { AuthorityButton } from "@/entities/menu-authority";
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
import { Button, Input, Label } from "@common/components";
import { SurveyApi } from "../api/survey-service";
import type { Survey, SurveyParams } from "../model/survey-interface";
import type { Survey } from "../model/survey-interface";
import { columns } from "../model/survey-table-columns";
type PostingTarget = {
@@ -22,32 +26,53 @@ type PostingTarget = {
pstgTrgtNo: number;
};
const DEFAULT_PARAMS = {
srvyTtl: "",
sort: "regDt,desc",
page: 0,
size: 10,
};
function SurveyPage() {
const router = useRouter();
const [srvyTtl, setSrvyTtl] = useState("");
const [searchParams, setSearchParams] = useState<SurveyParams>({
srvyTtl: "",
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const { params, setParams, resetParams } =
useSearchParamsState(DEFAULT_PARAMS);
const [srvyTtl, setSrvyTtl] = useState(params.srvyTtl);
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
const query = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.SURVEY, searchParams, pagination],
queryKey: [ENDPOINT.CMS_SERVICE.SURVEY, params],
queryFn: () =>
SurveyApi.getSurveyList({
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
srvyTtl: params.srvyTtl,
sort: params.sort,
page: params.page,
size: params.size,
}),
placeholderData: keepPreviousData,
});
const pagination: PaginationState = {
pageIndex: params.page,
pageSize: params.size,
};
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
const next = typeof updater === "function" ? updater(pagination) : updater;
setParams({ page: next.pageIndex, size: next.pageSize });
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setParams({ srvyTtl, page: 0 });
};
const handleReset = () => {
setSrvyTtl("");
resetParams();
};
const handleRowClick = (row: Survey) => {
router.push(`/survey/${row.srvySn}`);
};
@@ -62,7 +87,7 @@ function SurveyPage() {
},
manualPagination: true,
getCoreRowModel: getCoreRowModel(),
onPaginationChange: setPagination,
onPaginationChange: handlePaginationChange,
onRowSelectionChange: setRowSelection,
},
dataTableOptions: {
@@ -71,26 +96,6 @@ function SurveyPage() {
},
});
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
e.preventDefault();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
srvyTtl,
});
};
const handleReset = () => {
setSrvyTtl("");
setSearchParams({
...searchParams,
srvyTtl: "",
});
};
useEffect(() => {
const postingTargets = table
.getFilteredSelectedRowModel()
+44 -32
View File
@@ -2,58 +2,70 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
import { Button, Input, Label } from "@common/components";
import { CodeSelect } from "@/entities/code";
import {
getCoreRowModel,
type OnChangeFn,
type PaginationState,
} from "@tanstack/react-table";
import { ENDPOINT } from "@/shared/config";
import { useDataTable } from "@/shared/lib";
import { useDataTable, useSearchParamsState } from "@/shared/lib";
import { CodeSelect } from "@/entities/code";
import { Button, Input, Label } from "@common/components";
import { TemplateApi } from "../api/template-service";
import type { Template } from "../model/template-interface";
import { columns } from "../model/template-table-columns";
import type { Template, TemplateParams } from "../model/template-interface";
const DEFAULT_PARAMS = {
keyword: "",
tmpltSeCd: "",
sort: "regDt,desc",
page: 0,
size: 10,
};
function TemplatePage() {
const router = useRouter();
const [keyword, setKeyword] = useState("");
const [tmpltSeCd, setTmpltSeCd] = useState("");
const [searchParams, setSearchParams] = useState<TemplateParams>({
keyword: "",
tmpltSeCd: "",
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const { params, setParams, resetParams } =
useSearchParamsState(DEFAULT_PARAMS);
const [keyword, setKeyword] = useState(params.keyword);
const [tmpltSeCd, setTmpltSeCd] = useState(params.tmpltSeCd);
const query = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.TEMPLATE, searchParams, pagination],
queryKey: [ENDPOINT.CMS_SERVICE.TEMPLATE, params],
queryFn: () =>
TemplateApi.getTemplateList({
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
keyword: params.keyword,
tmpltSeCd: params.tmpltSeCd,
sort: params.sort,
page: params.page,
size: params.size,
}),
placeholderData: keepPreviousData,
});
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
const pagination: PaginationState = {
pageIndex: params.page,
pageSize: params.size,
};
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
const next = typeof updater === "function" ? updater(pagination) : updater;
setParams({ page: next.pageIndex, size: next.pageSize });
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setSearchParams({
...searchParams,
keyword,
tmpltSeCd,
});
setParams({ keyword, tmpltSeCd, page: 0 });
};
const handleReset = () => {
setKeyword("");
setTmpltSeCd("");
setSearchParams({
...searchParams,
keyword: "",
tmpltSeCd: "",
});
resetParams();
};
const handleRowClick = (row: Template) => {
@@ -67,7 +79,7 @@ function TemplatePage() {
state: {
pagination,
},
onPaginationChange: setPagination,
onPaginationChange: handlePaginationChange,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
},
+44 -37
View File
@@ -2,63 +2,70 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
import { Button, Input, Label } from "@common/components";
import { CodeSelect } from "@/entities/code";
import {
getCoreRowModel,
type OnChangeFn,
type PaginationState,
} from "@tanstack/react-table";
import { ENDPOINT } from "@/shared/config";
import { useDataTable } from "@/shared/lib";
import { useDataTable, useSearchParamsState } from "@/shared/lib";
import { CodeSelect } from "@/entities/code";
import { Button, Input, Label } from "@common/components";
import { TermsApi } from "../api/terms-service";
import type { Terms } from "../model/terms-interface";
import { columns } from "../model/terms-table-columns";
import type { TermsParams, Terms } from "../model/terms-interface";
const DEFAULT_PARAMS = {
keyword: "",
trmsSeCd: "",
sort: "regDt,desc",
page: 0,
size: 10,
};
function TermsPage() {
const router = useRouter();
const [keyword, setKeyword] = useState("");
const [trmsSeCd, setTrmsSeCd] = useState("");
const [searchParams, setSearchParams] = useState<TermsParams>({
keyword: "",
trmsSeCd: "",
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const { params, setParams, resetParams } =
useSearchParamsState(DEFAULT_PARAMS);
const [keyword, setKeyword] = useState(params.keyword);
const [trmsSeCd, setTrmsSeCd] = useState(params.trmsSeCd);
const query = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.TERMS, searchParams, pagination],
queryKey: [ENDPOINT.CMS_SERVICE.TERMS, params],
queryFn: () =>
TermsApi.getTermsList({
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
keyword: params.keyword,
trmsSeCd: params.trmsSeCd,
sort: params.sort,
page: params.page,
size: params.size,
}),
placeholderData: keepPreviousData,
});
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
const pagination: PaginationState = {
pageIndex: params.page,
pageSize: params.size,
};
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
const next = typeof updater === "function" ? updater(pagination) : updater;
setParams({ page: next.pageIndex, size: next.pageSize });
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
keyword,
trmsSeCd,
});
setParams({ keyword, trmsSeCd, page: 0 });
};
const handleReset = () => {
setKeyword("");
setTrmsSeCd("");
setSearchParams({
...searchParams,
keyword: "",
trmsSeCd: "",
});
resetParams();
};
const handleRowClick = (row: Terms) => {
@@ -72,7 +79,7 @@ function TermsPage() {
state: {
pagination,
},
onPaginationChange: setPagination,
onPaginationChange: handlePaginationChange,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
},
+68 -60
View File
@@ -3,19 +3,19 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import {
getCoreRowModel,
type OnChangeFn,
type PaginationState,
} from "@tanstack/react-table";
import dayjs from "dayjs";
import { ENDPOINT } from "@/shared/config";
import { useDataTable } from "@/shared/lib";
import { useDataTable, useSearchParamsState } from "@/shared/lib";
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
import { CodeApi } from "@/entities/code";
import {
UserApi,
type UserListResponse,
type UserSearchParams,
} from "@/entities/user";
import { UserApi, type UserListResponse } from "@/entities/user";
import {
Button,
Checkbox,
@@ -35,34 +35,39 @@ type DateRange = {
to?: Date | null;
};
const DEFAULT_PARAMS = {
keywordType: "all",
keyword: "",
userStateCode: "",
aprvYn: "",
grpId: "",
frDateYmd: "",
toDateYmd: "",
sort: "regDt,desc",
page: 0,
size: 10,
};
function parseDateRange(frDateYmd: string, toDateYmd: string): DateRange {
return {
from: frDateYmd ? dayjs(frDateYmd, "YYYYMMDD").toDate() : undefined,
to: toDateYmd ? dayjs(toDateYmd, "YYYYMMDD").toDate() : undefined,
};
}
function UsersPage() {
const router = useRouter();
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const { params, setParams, resetParams } =
useSearchParamsState(DEFAULT_PARAMS);
const [presetDateRange, setPresetDateRange] = useState("");
const [dateRange, setDateRange] = useState<DateRange>({
from: undefined,
to: undefined,
});
const [keywordType, setKeywordType] = useState("all");
const [keyword, setKeyword] = useState("");
const [userStateCode, setUserStateCode] = useState("");
const [aprvYn, setAprvYn] = useState("");
const [grpId, setGrpId] = useState("");
const [searchParams, setSearchParams] = useState<UserSearchParams>({
keywordType: "all",
keyword: "",
userStateCode: "",
aprvYn: "",
grpId: "",
frDateYmd: "",
toDateYmd: "",
sort: "regDt,desc",
});
const [dateRange, setDateRange] = useState<DateRange>(() =>
parseDateRange(params.frDateYmd, params.toDateYmd),
);
const [keywordType, setKeywordType] = useState(params.keywordType);
const [keyword, setKeyword] = useState(params.keyword);
const [userStateCode, setUserStateCode] = useState(params.userStateCode);
const [aprvYn, setAprvYn] = useState(params.aprvYn);
const [grpId, setGrpId] = useState(params.grpId);
const { data: userSttsStats } = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.USERS, "userStatusStatistics"],
@@ -70,13 +75,21 @@ function UsersPage() {
});
const query = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.USERS, searchParams, pagination],
queryKey: [ENDPOINT.USER_SERVICE.USERS, params],
queryFn: () =>
UserApi.findPageUser({
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
keywordType: params.keywordType,
keyword: params.keyword,
userStateCode: params.userStateCode,
aprvYn: params.aprvYn,
grpId: params.grpId,
frDateYmd: params.frDateYmd,
toDateYmd: params.toDateYmd,
sort: params.sort,
page: params.page,
size: params.size,
}),
placeholderData: keepPreviousData,
});
const { data: userStateCodes } = useQuery({
@@ -84,6 +97,16 @@ function UsersPage() {
queryFn: () => CodeApi.getCode("USERSTTS"),
});
const pagination: PaginationState = {
pageIndex: params.page,
pageSize: params.size,
};
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
const next = typeof updater === "function" ? updater(pagination) : updater;
setParams({ page: next.pageIndex, size: next.pageSize });
};
const handleRowClick = (row: UserListResponse) => {
router.push(`/users/${row.userUnqId}`);
};
@@ -97,7 +120,7 @@ function UsersPage() {
},
manualPagination: true,
getCoreRowModel: getCoreRowModel(),
onPaginationChange: setPagination,
onPaginationChange: handlePaginationChange,
},
dataTableOptions: {
onRowClick: handleRowClick,
@@ -105,14 +128,9 @@ function UsersPage() {
},
});
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
setParams({
keywordType,
keyword,
userStateCode,
@@ -122,6 +140,7 @@ function UsersPage() {
? dayjs(dateRange.from).format("YYYYMMDD")
: "",
toDateYmd: dateRange?.to ? dayjs(dateRange.to).format("YYYYMMDD") : "",
page: 0,
});
};
@@ -131,20 +150,12 @@ function UsersPage() {
setUserStateCode("");
setAprvYn("");
setGrpId("");
setPresetDateRange("");
setDateRange({
from: undefined,
to: undefined,
});
setSearchParams({
...searchParams,
keywordType: "all",
keyword: "",
userStateCode: "",
aprvYn: "",
grpId: "",
frDateYmd: "",
toDateYmd: "",
});
resetParams();
};
const handlePresetDateRange = (value: string) => {
@@ -351,12 +362,9 @@ function UsersPage() {
<h3 className="text-2xl font-medium text-label">회원 목록</h3>
<div className="flex gap-2">
<Select
defaultValue="regDt,desc"
value={params.sort}
onValueChange={(value) => {
setSearchParams({
...searchParams,
sort: value,
});
setParams({ sort: value });
}}
>
<SelectTrigger>
+4 -3
View File
@@ -1,8 +1,9 @@
export { getCookie, setCookie, removeCookie } from "./cookie";
export { useMutationWrapper } from "./use-mutation-wrapper";
export { getCookie, removeCookie, setCookie } from "./cookie";
export { useAlertStore } from "./use-alert-store";
export { useDataTable } from "./use-data-table";
export { useMenuStore } from "./use-menu-store";
export { useUserStore } from "./use-user-store";
export { useMutationWrapper } from "./use-mutation-wrapper";
export { usePrompt } from "./use-prompt";
export { useSearchParamsState } from "./use-search-params-state";
export { useSelectPrompt } from "./use-select-prompt";
export { useUserStore } from "./use-user-store";
@@ -0,0 +1,90 @@
"use client";
import { useCallback, useMemo, useRef } from "react";
import { useSearchParams } from "next/navigation";
/**
* 조회 조건을 URL searchParams와 동기화하는 훅.
*
* - URL이 단일 소스 오브 트루스(SSOT)이며, 새로고침·뒤로가기 시에도 조건이 유지된다.
* - 기본값과 동일한 값은 URL에서 제거해 쿼리스트링을 깨끗하게 유지한다.
* - `window.history.replaceState` 기반 shallow 갱신으로 서버 라운드트립 없이 동작한다.
* - 숫자 기본값이 주어진 키는 자동으로 `Number` 파싱한다.
*
* @example
* const { params, setParams, resetParams } = useSearchParamsState({
* keyword: "",
* page: 0,
* size: 10,
* });
*/
export function useSearchParamsState<T extends Record<string, ParamValue>>(
defaults: T,
): UseSearchParamsStateReturn<T> {
const searchParams = useSearchParams();
const defaultsRef = useRef(defaults);
const params = useMemo(
() => parseParams(searchParams, defaultsRef.current),
[searchParams],
);
const setParams = useCallback((partial: Partial<T>) => {
const next = new URLSearchParams(window.location.search);
for (const [key, value] of Object.entries(partial)) {
if (value === undefined || value === defaultsRef.current[key]) {
next.delete(key);
} else {
next.set(key, String(value));
}
}
replaceUrl(next);
}, []);
const resetParams = useCallback(() => {
const next = new URLSearchParams(window.location.search);
for (const key of Object.keys(defaultsRef.current)) {
next.delete(key);
}
replaceUrl(next);
}, []);
return { params, setParams, resetParams };
}
function parseParams<T extends Record<string, ParamValue>>(
searchParams: ReturnType<typeof useSearchParams>,
defaults: T,
): T {
const result = { ...defaults };
for (const key of Object.keys(defaults) as (keyof T)[]) {
const raw = searchParams?.get(key as string) ?? null;
if (raw === null) continue;
if (typeof defaults[key] === "number") {
const parsed = Number(raw);
if (!Number.isNaN(parsed)) result[key] = parsed as T[keyof T];
} else {
result[key] = raw as T[keyof T];
}
}
return result;
}
function replaceUrl(searchParams: URLSearchParams) {
const qs = searchParams.toString();
const url = qs
? `${window.location.pathname}?${qs}`
: window.location.pathname;
window.history.replaceState(null, "", url);
}
type ParamValue = string | number | null;
interface UseSearchParamsStateReturn<T> {
params: T;
setParams: (partial: Partial<T>) => void;
resetParams: () => void;
}
+11 -6
View File
@@ -1,11 +1,13 @@
import { toast } from "sonner";
import { useEffect } from "react";
import { useRouter, usePathname } from "next/navigation";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useClipboard } from "@frontend-opensource/use-react-hooks";
import { Button } from "@common/components";
import { deleteReasonMessages } from "@/entities/post";
import { AuthorityButton } from "@/entities/menu-authority";
import { toast } from "sonner";
import { useMenuStore, useSelectPrompt, useUserStore } from "@/shared/lib";
import { AuthorityButton } from "@/entities/menu-authority";
import { deleteReasonMessages } from "@/entities/post";
import { Button } from "@common/components";
type PostActionProps = {
bbsId: string;
@@ -21,11 +23,14 @@ const selectData = Object.entries(deleteReasonMessages).map(
function PostAction({ children, handleDelete }: PostActionProps) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const { copyText, copied } = useClipboard();
const { prompt } = useSelectPrompt(selectData);
const bbsInfo = useMenuStore((state) => state.bbsInfo);
const userInfo = useUserStore((state) => state.userInfo);
const postPageLink = `/bbs/${bbsInfo?.bbsTypeCd.toLowerCase()}/${bbsInfo?.bbsId}`;
const from = searchParams?.get("from");
const backLink = from ?? postPageLink;
const onDelete = async () => {
const { resolved, value } = await prompt("삭제 사유를 입력해주세요.");
@@ -65,7 +70,7 @@ function PostAction({ children, handleDelete }: PostActionProps) {
size="lg"
color="white"
type="button"
onClick={() => router.push(postPageLink)}
onClick={() => router.push(backLink)}
>
목록
</Button>
+2 -2
View File
@@ -18,8 +18,8 @@ NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:800
NEXT_PUBLIC_NGII_URL = http://172.29.143.87/poiact
NEXT_PUBLIC_NGII_PARAM = BDE2BEF39DAFEC293AB6730150F222AC&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={y}&tilerow={x}
NEXT_PUBLIC_GATEWAY_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
CONTENTS_INTRO = 31
CONTENTS_EXAMPLE = 32
CONTENTS_INTRO = 24
CONTENTS_EXAMPLE = 25
NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do
NEXT_PRIVATE_WR_ADDRESS_KEY = U01TXOFVVEgyMDI1MDgyODE0MTAxNzExNjEyMjA=
@@ -365,7 +365,7 @@ export default function SimilarCaseModal({
</tr>
<tr>
<th rowSpan={2} className={thBase}>
총대장균군수<br />(개/100ml)
총대장균군수<br />(개/100mL)
</th>
<th rowSpan={2} className={thBase}>
용도별 결합 잔류염소<br />(mg/L)
@@ -428,7 +428,7 @@ export default function DemandRegisterPage() {
</FormRow>
<Divider />
<FormRow label="요구수량(m²/d)" required>
<FormRow label="요구수량(㎥/d)" required>
<TextInput value={reqQty} onChange={setReqQty} />
</FormRow>
<Divider />
@@ -438,7 +438,7 @@ export default function DemandRegisterPage() {
</FormRow>
<Divider />
<FormRow label="희망단가(원)" required>
<FormRow label="희망단가(원/㎥)" required>
<TextInput value={hopePrice} onChange={setHopePrice} />
</FormRow>
</div>
@@ -464,7 +464,7 @@ export default function DemandRegisterPage() {
<div className="border border-neutral-200 bg-white">
<QualityRow
leftLabel="총대장균군수(개/mg)"
leftLabel="총대장균군수(개/100㎖)"
leftRequired
leftControl={
<div className="flex items-center gap-4">
@@ -148,7 +148,7 @@ export default function DemandStatusPage() {
{ label: "요구시기", value: selected.detail.reqDate, required: true },
{ label: "요구수량(㎡/d)", value: selected.detail.reqQty },
{ label: "사용용도", value: selected.detail.usePurpose },
{ label: "희망단가(원)", value: selected.detail.price },
{ label: "희망단가(원/㎥)", value: selected.detail.price },
];
}, [selected]);
@@ -157,7 +157,7 @@ export default function DemandStatusPage() {
if (!selected) return [];
const w = selected.detail.water;
return [
{ label: "총대장균군수(개/100mℓ)", value: w.coliform },
{ label: "총대장균군수(개/100㎖)", value: w.coliform },
{ label: "결합잔류염소(mg/L)", value: "-" },
{ label: "탁도(NTU)", value: w.turbidity },
@@ -44,17 +44,17 @@ export default function DemandRegisterPage() {
const [coliform, setColiform] = useState<string>(""); // 총대장균군수
const [noDetect, setNoDetect] = useState<boolean>(false);
const [tn, setTn] = useState<string>("");
const [residualCl, setResidualCl] = useState<string>("");
const [tp, setTp] = useState<string>("");
const [turbidity, setTurbidity] = useState<string>("");
const [ph, setPh] = useState<PHRange>({ min: "", max: "" });
const [bod, setBod] = useState<string>("");
const [chloride, setChloride] = useState<string>("");
const [odor, setOdor] = useState<string>("불쾌하지 않을 것");
const [ec, setEc] = useState<string>("");
const [color, setColor] = useState<string>("");
const [remark, setRemark] = useState<string>("");
const [tn, setTn] = useState<string>(""); // 총질소(T-N)
const [residualCl, setResidualCl] = useState<string>(""); // 결합잔류염소
const [tp, setTp] = useState<string>(""); // 총인(T-P)
const [turbidity, setTurbidity] = useState<string>(""); // 탁도
const [ph, setPh] = useState<PHRange>({ min: "", max: "" }); // 수소이온농도(pH)
const [bod, setBod] = useState<string>(""); // 생물학적산소요구량(BOD)
const [chloride, setChloride] = useState<string>(""); // 염화물
const [odor, setOdor] = useState<string>("불쾌하지 않을 것"); // 냄새
const [ec, setEc] = useState<string>(""); // 전기전도도
const [color, setColor] = useState<string>(""); // 색도
const [remark, setRemark] = useState<string>(""); // 비고
// 공급처 후보지 선택(필수아님)
const [cand1, setCand1] = useState("");
@@ -373,7 +373,8 @@ export default function DemandRegisterPage() {
switch (value) {
// 청소화장실용도
case '01':
setTp('0.2');
setNoDetect(true); // 총대장균군수 불검출
setResidualCl('0.2'); // 잔류염소
setTurbidity('2');
setPh({ min: '5.8', max: '8.5' });
setBod('5');
@@ -381,23 +382,35 @@ export default function DemandRegisterPage() {
break;
// 세척살수용도
case '02':
setTp('0.2');
setTurbidity('2');
setColiform('1000');
setTurbidity('2'); // 탁도
setPh({ min: '5.8', max: '8.5' });
setBod('5');
break;
// 조경용도
case '03':
setTp('0.2');
setColiform('1000');
setTurbidity('2');
setPh({ min: '5.8', max: '8.5' });
setBod('5');
setChloride('250');
setChloride('250'); // 염화물
break;
// 지하수
// 친수용도
case '04':
setNoDetect(true); // 총대장균군수 불검출
setResidualCl('0.1'); // 잔류염소
setTurbidity('2');
setBod('3');
setColor('10');
setTn('10'); // 총질소
setTp('0.5'); // 총인
setPh({ min: '5.8', max: '8.5' });
setChloride('0.5');
break;
// 지하수충전
case '05':
break;
// 농업용도
// 농업용수
case '06':
setTurbidity('5');
setPh({ min: '5.8', max: '8.5' });
@@ -405,16 +418,18 @@ export default function DemandRegisterPage() {
setEc('2000');
setFarmText("이하 (** 간접:2000,직접:700 )");
break;
// 하천 등 유지보수
// 하천 등 유지용수
case '07':
setColiform('1000');
setTn('10');
setBod('5');
setTn('20');
setTp('0.5');
setPh({ min: '5.8', max: '8.5' });
setBod('5');
setChloride('0.5');
break;
// 공업
// 공업용수
case '08':
setBod('20');
setPh({ min: '5.8', max: '8.5' });
break;
default:
@@ -467,7 +482,7 @@ export default function DemandRegisterPage() {
<Divider />
<FormRow label="요구수량(m²/d)" required>
<FormRow label="요구수량(㎥/d)" required>
<TextInput value={reqQty} onChange={setReqQty} placeholder="" />
</FormRow>
@@ -490,7 +505,7 @@ export default function DemandRegisterPage() {
<Divider />
<FormRow label="희망단가(원)" required>
<FormRow label="희망단가(원/㎥)" required>
<TextInput value={hopePrice} onChange={setHopePrice} />
</FormRow>
</div>
@@ -519,7 +534,7 @@ export default function DemandRegisterPage() {
<div className="border border-neutral-200 bg-white">
{/* 1행 */}
<QualityRow
leftLabel="총대장균군수(개/mg)"
leftLabel="총대장균군수(개/100㎖)"
leftControl={
<div className="flex items-center gap-4">
@@ -30,6 +30,19 @@ const formatValue = (value: string | number | null | undefined) => {
return String(value);
};
const formatNumberWithComma = (value: string | number | null | undefined) => {
if (value === null || value === undefined || value === "") return "-";
const normalized =
typeof value === "string" ? Number(value.replace(/,/g, "")) : Number(value);
if (Number.isNaN(normalized)) {
return String(value);
}
return normalized.toLocaleString("ko-KR");
};
const formatBelow = (value: string | number | null | undefined) => {
if (value === null || value === undefined || value === "") return "-";
if (Number(value) === -1) return "불검출";
@@ -234,9 +247,9 @@ export default function DemandStatusPage() {
return [
{ label: "요구시기", key: "dmndTmng", value: formatDate(selectedSdmInfoStatus.dmndTmng), required: true },
{ label: "요구수량(㎥/d)", key: "dmndQty", value: formatValue(selectedSdmInfoStatus.dmndQty) },
{ label: "요구수량(㎥/d)", key: "dmndQty", value: formatNumberWithComma(selectedSdmInfoStatus.dmndQty) },
{ label: "사용용도", key: "useUsgCd", value: formatValue(selectedSdmInfoStatus.useUsgCd) },
{ label: "희망단가(원/㎥)", key: "wntUntprc", value: formatValue(selectedSdmInfoStatus.wntUntprc) },
{ label: "희망단가(원/㎥)", key: "wntUntprc", value: formatNumberWithComma(selectedSdmInfoStatus.wntUntprc) },
];
}, [selectedSdmInfoStatus]);
@@ -244,7 +257,7 @@ export default function DemandStatusPage() {
if (!selectedSdmInfoStatus) return [];
return [
{ label: "총대장균군수(개/100mL)", key: "grssClfg", value: formatBelow(selectedSdmInfoStatus.grssClfg) },
{ label: "총대장균군수(개/100㎖)", key: "grssClfg", value: formatBelow(selectedSdmInfoStatus.grssClfg) },
{ label: "결합잔류염소(mg/L)", key:"combResdch", value: formatBelow(selectedSdmInfoStatus.combResdch) },
{ label: "탁도(NTU)", key:"trbt", value: formatBelow(selectedSdmInfoStatus.trbt) },
@@ -520,7 +533,7 @@ export default function DemandStatusPage() {
{!selectedSdmInfoStatus ? (
<div className="mt-3 border border-neutral-200 px-6 py-10 text-center text-sm text-slate-400">
상세 정보를 표시할 신청건이 없습니다.
조회된 결과가 없습니다.
</div>
) : (
<div className="mt-3 border border-neutral-200">
+1 -1
View File
@@ -17,7 +17,7 @@ type IntroCardItem = {
const INTRO_CARDS: IntroCardItem[] = [
{
title: "물 재이용 법",
title: "물재이용법",
desc: "물의 재이용 촉진 및 지원에 관한 법률을 시행하고, 일부 법률은 개정되었습니다.",
image: card01,
href: "/intro#law-plan",
+1 -1
View File
@@ -770,7 +770,7 @@ export const SdmReducer = (set: any): SdmActions => {
"/cms-service/api/v1/lwpo-cds/?lwpoCdNm=&sort=regDt,desc&upCd=WR018&page=0&size=100"
);
const comUseTypes = upCd.content.filter(el=>el.lwpoCdNm != '친수용수').map((el: any) => ({ text: el.lwpoCdNm, val: el.lwpoCd }));
const comUseTypes = upCd.content.map((el: any) => ({ text: el.lwpoCdNm, val: el.lwpoCd }));
set((state: SdmType) => ({
Sdm: {
+8 -8
View File
@@ -83,14 +83,14 @@ const MOCK: Record<TabKey, CaseItem[]> = {
sewage: [
{
id: "sewage-1",
title: "구리시 하수 재이용",
title: "구리시 하수처리수 재이용",
desc: "고농도 미세먼지 발생 및 폭염의 급증에 따라 공공하수처리시설 발생 재이용수 일부를 도로자동청소시스템에 활용하여 미세먼지 저감 및 폭염 취약성 완화",
imageSrc: "/images/landing/business/sewage/guri_01.png",
href: "/example#example-sewage-guri",
},
{
id: "sewage-2",
title: "부천시 하수재이용",
title: "부천시 하수처리수 재이용",
desc: "굴포하수처리시설 하수처리수 재이용수 생산시설 설치 운영을 통해 여월천 등 부천 관내 하천의 건천화 방지, 하천 생태계 보존 및 주민들에게 친수 공간 제공",
imageSrc: "/images/landing/business/sewage/bucheon_01.png",
href: "/example#example-sewage-bucheon",
@@ -104,42 +104,42 @@ const MOCK: Record<TabKey, CaseItem[]> = {
},
{
id: "sewage-4",
title: "파주시 하수 재이용 시설",
title: "파주시 하수처리수 재이용",
desc: "운정 및 금촌하수처리시설의 하수처리수를 막공법으로 재처리하여 파주LCD산업단지 내 3개 업체에 공업용수를 공급(LG디스플레이, 희성전자, 엔비텍)",
imageSrc: "/images/landing/business/sewage/paju_01.png",
href: "/example#example-sewage-paju",
},
{
id: "sewage-5",
title: "포항시 하수 재이용 시설",
title: "포항시 하수처리수 재이용",
desc: "포항시 내 공단 개발 확대로 지속적이고 안정적인 공업용수 공급 추진(포스코, 동국산업 등), 포항하수처리장 하수처리수를 재처리하여 공업용수로 공급",
imageSrc: "/images/landing/business/sewage/pohang_01.png",
href: "/example#example-sewage-pohang",
},
{
id: "sewage-6",
title: "울산광역시 하수 재이용 시설",
title: "울산광역시 하수처리수 재이용",
desc: "18년 갈수기시 낙동강 수질 악화로 울산석유화학단지 용수 공급 부족을 해소하기 위해 미포산단 내 한화케미칼 등 8개사 공업용수 공급",
imageSrc: "/images/landing/business/sewage/ulsan_01.png",
href: "/example#example-sewage-ulsan",
},
{
id: "sewage-7",
title: "여수시 하수 재이용 시설",
title: "여수시 하수처리수 재이용",
desc: "수요자가 원하는 양질의 공업용수를 저렴하게 공급하여 한정된 수자원의 효율적 이용 및 지역적 물 부족 해소, 하수재이용을 통한 오염총량 저감 및 방류수역 수질개선을 위한 여수국가산단 내 공업용수 공급(GS칼텍스, YNCC 등)",
imageSrc: "/images/landing/business/sewage/yeosu_01.png",
href: "/example#example-sewage-yeosu",
},
{
id: "sewage-8",
title: "보령시 하수 재이용 시설",
title: "보령시 하수처리수 재이용",
desc: "한국중부발전(주)의 보령 및 신보령화력발전소에 10,000㎥/일의 탈황용수 공급을 위해 보령하수처리시설의 하수처리수 재이용 사업 추진, 하수처리수 재이용을 통한 수자원 절약 및 지역 물 부족 해소",
imageSrc: "/images/landing/business/sewage/boryeong_01.png",
href: "/example#example-sewage-boryeong",
},
{
id: "sewage-9",
title: "아산시 하수 재이용 시설",
title: "아산시 하수처리수 재이용",
desc: "신도시 개발에 따른 인구 급증으로 발생하는 하수를 재처리하여 탕정산업단지(삼성디스플레이)에 공업용수를 안정적으로 공급함으로써 지역기반 및 활성화를 도모",
imageSrc: "/images/landing/business/sewage/asan_01.png",
href: "/example#example-sewage-asan",
@@ -282,7 +282,7 @@
// },
// {
// name: "money",
// label: "희망단가(원)",
// label: "희망단가(원/㎥)",
// formIndex: 0,
// required: true,
// component: (field) => (
@@ -375,7 +375,7 @@
// render={({ field }) =>
// name === "money" ? (
// <div>
// <Label className="text-black">희망단가(원)*</Label>
// <Label className="text-black">희망단가(원/㎥)*</Label>
// {component(field)}
// </div>
// ) : (
@@ -411,7 +411,7 @@
// return (
// <FormUi
// label={
// !isDisabled ? `총대장균군수(개/mg)*` : "총대장균군수(개/mg)"
// !isDisabled ? `총대장균군수(개/100㎖)*` : "총대장균군수(개/100㎖)"
// }
// >
// <div className="flex items-center gap-2">
+1
View File
@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 60 KiB

@@ -237,7 +237,7 @@ export default function FacilityTermModalHeader<T extends { msrDt: string }>({
onSelectValue={(val) => setFlowPanel(val.toString())}
className="mx-3 w-[150px]"
label=""
selectClass="w-full p-1 border rounded bg-white"
selectClass="w-full rounded border border-input bg-form p-1"
/>
</div>
</TableCell>
@@ -251,7 +251,7 @@ export default function FacilityTermModalHeader<T extends { msrDt: string }>({
mode="single"
selected={startDate}
onSelect={(date) => handleStartDateSelect(date as Date)}
className="w-full p-1 border rounded bg-white"
className="w-full rounded border border-input bg-form p-1 text-foreground"
disabled={(date) => !isDateInRanges(date)}
defaultMonth={startDate}
/>
@@ -262,7 +262,7 @@ export default function FacilityTermModalHeader<T extends { msrDt: string }>({
mode="single"
selected={endDate}
onSelect={(date) => handleEndDateSelect(date as Date)}
className="w-full p-1 border rounded bg-white"
className="w-full rounded border border-input bg-form p-1 text-foreground"
disabled={(date) => !isDateInRanges(date)}
defaultMonth={endDate}
/>
@@ -275,4 +275,4 @@ export default function FacilityTermModalHeader<T extends { msrDt: string }>({
</div>
</ListDiv>
);
}
}
@@ -219,7 +219,7 @@ export default function FacilityTermModalHeaderMonth<T extends { msrDt: string }
return (
<ListDiv className="!h-[100px]">
<div className="card !m-[1.2rem] max-h-[40rem] overflow-auto !p-0r">
<div className="card !m-[1.2rem] max-h-[40rem] overflow-auto !p-0">
<Table>
<thead>
<TableRow>
@@ -237,7 +237,7 @@ export default function FacilityTermModalHeaderMonth<T extends { msrDt: string }
onSelectValue={(val) => setFlowPanel(val.toString())}
className="mx-3 w-[150px]"
label=""
selectClass="w-full p-1 border rounded bg-white"
selectClass="w-full rounded border border-input bg-form p-1"
/>
</div>
</TableCell>
@@ -251,7 +251,7 @@ export default function FacilityTermModalHeaderMonth<T extends { msrDt: string }
mode="single"
selected={startDate}
onSelect={(date) => handleStartDateSelect(date as Date)}
className="w-full p-1 border rounded bg-white"
className="w-full rounded border border-input bg-form p-1 text-foreground"
defaultMonth={startDate}
/>
</div>
@@ -261,7 +261,7 @@ export default function FacilityTermModalHeaderMonth<T extends { msrDt: string }
mode="single"
selected={endDate}
onSelect={(date) => handleEndDateSelect(date as Date)}
className="w-full p-1 border rounded bg-white"
className="w-full rounded border border-input bg-form p-1 text-foreground"
defaultMonth={endDate}
/>
</div>
@@ -273,4 +273,4 @@ export default function FacilityTermModalHeaderMonth<T extends { msrDt: string }
</div>
</ListDiv>
);
}
}
@@ -5,55 +5,55 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
return (
layoutNum === 'A' ? (
<>
<TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) =>
<TableRow className="bg-secondary">{upHeadList.map((head: TableUpperProps) =>
(head.id === 'trtFrfNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> :
head.id === 'laeNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> :
head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? <TableHead rowSpan={2} className="w-40" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> :
head.id === 'trstOprEtpNm' ? <TableHead className="w-1/15" rowSpan={2} key={head.id}>{head.title}</TableHead> :
head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={2}>{'행정구역'}</TableHead> :
!head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-gray-20" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={2}>{'행정구역'}</TableHead> :
!head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-accent text-accent-foreground" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
</TableRow>
<TableRow className="bg-gray-50">
<TableRow className="bg-secondary">
{upHeadList.map((head: TableUpperProps) => {
if (head.upName === 'upChangeRe') {
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>;
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-secondary">{head.title}</TableHead>;
}
})}
</TableRow>
</>
) : layoutNum === 'R' ? (
<>
<TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) =>
<TableRow className="bg-secondary">{upHeadList.map((head: TableUpperProps) =>
(head.id === 'trtFrfNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> :
head.id === 'laeNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> :
head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? <TableHead rowSpan={2} className="w-40" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> :
head.id === 'trstOprEtpNm' ? <TableHead className="w-1/15" rowSpan={2} key={head.id}>{head.title}</TableHead> :
head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={3}>{'유역'}</TableHead> :
!head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-gray-20" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={3}>{'유역'}</TableHead> :
!head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-accent text-accent-foreground" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
</TableRow>
<TableRow className="bg-gray-50">
<TableRow className="bg-secondary">
{upHeadList.map((head: TableUpperProps) => {
if (head.upName === 'upChangeRe') {
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>;
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-secondary">{head.title}</TableHead>;
}
})}
</TableRow>
</>
) : (
<>
<TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) =>
<TableRow className="bg-secondary">{upHeadList.map((head: TableUpperProps) =>
(head.id === 'trtFrfNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> :
head.id === 'laeNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> :
head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? <TableHead rowSpan={2} className="w-40" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> :
head.id === 'trstOprEtpNm' ? <TableHead className="w-1/15" rowSpan={2} key={head.id}>{head.title}</TableHead> :
head.upName === 'upChangeRe1' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={3}>{'권역'}</TableHead> :
head.upName === 'upChangeRe2' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={2}>{'행정구역'}</TableHead> :
!head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-gray-20" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
head.upName === 'upChangeRe1' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={3}>{'권역'}</TableHead> :
head.upName === 'upChangeRe2' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={2}>{'행정구역'}</TableHead> :
!head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-accent text-accent-foreground" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
</TableRow>
<TableRow className="bg-gray-50">
<TableRow className="bg-secondary">
{upHeadList.map((head: TableUpperProps) => {
if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2') {
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>;
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-secondary">{head.title}</TableHead>;
}
})}
</TableRow>
@@ -99,22 +99,22 @@ export default function SearchInputText({label=undefined, setValue=()=>{}, sidoS
return (
<div className={'relative'} ref={wrapperRef}>
{label && <Label className="w-20 text-xl text-gray-600 justify-self-center">{label}</Label>}
{label && <Label className="w-20 justify-self-center text-xl text-label">{label}</Label>}
<input
type="text"
value={textValue}
onChange={handleInputChange}
onFocus={onInputFocus}
className="w-[200px] ml-6 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
className="ml-6 w-[200px] rounded-md border border-input bg-form px-3 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
placeholder={`${label}을(를) 입력하세요`}
/>
{isOpen && processFacilityList.length > 0 && (
<div className="absolute z-10 w-[300px] px-2 py-1 !mt-5 ml-20 bg-white border border-gray-300 rounded-md shadow-lg h-90 overflow-auto">
<div className="absolute z-10 ml-20 !mt-5 h-[90px] w-[300px] overflow-auto rounded-md border border-border bg-popover px-2 py-1 text-popover-foreground shadow-lg">
<div className="max-h-72">
{processFacilityList.map((item : facilityListProps, index : number) => (
<div
key={index}
className="w-full text-left pl-1 pr-10 py-2 hover:bg-gray-100 cursor-pointer overflow-auto"
className="w-full cursor-pointer overflow-auto py-2 pl-1 pr-10 text-left hover:bg-accent hover:text-accent-foreground"
onClick={() => handleSuggestionClick(item.bscFctCd, item.bscFctNm, Number(item.fctCpc), item.trtFrfNm, item.efflWqltStndrdNm)}
role="button"
tabIndex={0}
@@ -17,7 +17,7 @@ function UnderComment({commentStartPoint="☞", commentArray=[], divHeight="14"}
<div className={"mt-"+divHeight}>
{commentArray.map((item, index) => (
<React.Fragment key={"comment" + index}>
<span className="text-2xl font-bold text-gray-500">
<span className="text-2xl font-bold text-muted-foreground">
{commentStartPoint} {item}
</span>
<br/>
@@ -28,4 +28,4 @@ function UnderComment({commentStartPoint="☞", commentArray=[], divHeight="14"}
}
}
export default UnderComment;
export default UnderComment;
+7 -4
View File
@@ -127,15 +127,18 @@ const Map = () => {
}
const el = overlayContainerRef.current;
el.className =
"ol-custom-overlay shadow-xl rounded-2xl bg-white/95 backdrop-blur p-3 border border-gray-200 min-w-[220px] max-w-[350px] ";
"ol-custom-overlay shadow-xl rounded-2xl backdrop-blur p-3 min-w-[220px] max-w-[350px]";
el.style.backgroundColor = "color-mix(in srgb, var(--popover) 95%, transparent)";
el.style.color = "var(--popover-foreground)";
el.style.border = "1px solid var(--border)";
el.innerHTML = `
<div class="font-semibold text-lg text-gray-800 mb-2 text-center bg-green-400">${title}</div>
<table class="w-full text-base text-gray-800">
<div class="mb-2 rounded-md px-2 py-1 text-center text-lg font-semibold" style="background-color: var(--secondary); color: var(--secondary-foreground);">${title}</div>
<table class="w-full text-base" style="color: var(--foreground);">
${Object.entries(props)
.map(
([k, v]) => `
<tr>
<td class="py-1 pr-2 text-gray-500 whitespace-nowrap">${k}</td>
<td class="whitespace-nowrap py-1 pr-2" style="color: var(--muted-foreground);">${k}</td>
<td class="py-1 font-medium">${safe(v)}</td>
</tr>`
)
@@ -54,23 +54,23 @@ export default function DrainageEquipment({searchYear = '', fcltyList = []}: Dra
const HeadMakeColSpan = ({ upHeadList }: { upHeadList: TableUpperProps[] }) => {
return (
<>
<TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) =>
<TableRow className="bg-secondary">{upHeadList.map((head: TableUpperProps) =>
(head.id === 'part1' ? <TableHead rowSpan={3} className='w-60' key={head.id}>구분</TableHead> :
head.id === 'part2' ? <TableHead rowSpan={3} className='w-60' key={head.id}>구분</TableHead> :
head.id == 'nowSum' ? <TableHead rowSpan={2} colSpan={3} key={head.id} style={{ whiteSpace: 'pre-line' }}>{'현재\n(기준년도)'}</TableHead> :
head.id == 'stepOneSum' ? <TableHead rowSpan={1} colSpan={12} key={head.id} style={{ whiteSpace: 'pre-line' }}>{'변경(금회계획)'}</TableHead> :
head.id == 'etc' ? <TableHead rowSpan={3} key={head.id}>비고</TableHead> : null))}
</TableRow>
<TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) =>
<TableRow className="bg-secondary">{upHeadList.map((head: TableUpperProps) =>
(head.id === 'stepOneSum' ? <TableHead rowSpan={1} colSpan={3} key={head.id}>1단계</TableHead> :
head.id === 'stepTwoSum' ? <TableHead rowSpan={1} colSpan={3} key={head.id}>2단계</TableHead> :
head.id === 'stepThreeSum' ? <TableHead rowSpan={1} colSpan={3} key={head.id}>3단계</TableHead> :
head.id === 'stepFourSum' ? <TableHead rowSpan={1} colSpan={3} key={head.id}>4단계</TableHead> : null))}
</TableRow>
<TableRow className="bg-gray-50">
<TableRow className="bg-secondary">
{upHeadList.map((head: TableUpperProps) => {
if (head.upName === 'upChangeRe2' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4' || head.upName === 'upChangeRe5' || head.upName === 'upChangeRe6') {
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>;
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-secondary">{head.title}</TableHead>;
}
})}
</TableRow>
@@ -147,7 +147,7 @@ export default function DashBoardChartModal({item='', period='', title='',modalD
if (label) {
config[label] = {
label: label,
color : "hsl(var(--chart-1))"
color : "var(--chart-1)"
};
}
return config;
@@ -32,12 +32,12 @@ const headMakeColSpanPlanFclty = (upHeadList: TableUpperProps[], planYearList: s
return ( <>
<TableRow>{
upHeadList.map((head: TableUpperProps) =>
(!head.upSequnce?<TableHead rowSpan={2}key={head.id}>{head.title}</TableHead>:
head.upName === 'upChangeRe'&& head.upSequnce=== 1?<TableHead key={head.id} rowSpan={1} colSpan={7} style={{ whiteSpace: 'pre-line' }}>{'시설계획\n(금회)'}</TableHead>:null))}
(!head.upSequnce?<TableHead className="fo-facility-info-header-surface" rowSpan={2}key={head.id}>{head.title}</TableHead>:
head.upName === 'upChangeRe'&& head.upSequnce=== 1?<TableHead className="fo-facility-info-header-surface" key={head.id} rowSpan={1} colSpan={7} style={{ whiteSpace: 'pre-line' }}>{'시설계획\n(금회)'}</TableHead>:null))}
</TableRow>
<TableRow>
{upHeadList.map((head: TableUpperProps, index: number) =>
(head.upName === 'upChangeRe'?<TableHead key={head.id} style={{ whiteSpace: 'pre-line' }}>{index===0?head.title:head.title+'\n('+planYearList[index-1]+')'}</TableHead>:null))}
(head.upName === 'upChangeRe'?<TableHead className="fo-facility-info-header-surface" key={head.id} style={{ whiteSpace: 'pre-line' }}>{index===0?head.title:head.title+'\n('+planYearList[index-1]+')'}</TableHead>:null))}
</TableRow>
</> )
}
@@ -52,20 +52,20 @@ const headMakeColSpanPlanFclty = (upHeadList: TableUpperProps[], planYearList: s
const headMakeColSpanOperationStatus = (upHeadList: TableUpperProps[], searchYear: string) => {
return ( <>
<TableRow>
<TableHead rowSpan={1} colSpan={17} style={{ whiteSpace: 'pre-line' }}>{searchYear+'년도 운영실태'}</TableHead>
<TableHead className="fo-facility-info-header-surface" rowSpan={1} colSpan={17} style={{ whiteSpace: 'pre-line' }}>{searchYear+'년도 운영실태'}</TableHead>
</TableRow>
<TableRow>{
upHeadList.map((head: TableUpperProps) =>
(head.upName === 'upChangeRe1'&&head.upSequnce===1?<TableHead key={head.id} rowSpan={2}colSpan={2} style={{ whiteSpace: 'pre-line' }}>{head.title}</TableHead>:
!head.upSequnce?<TableHead style={{ whiteSpace: 'pre-line' }} rowSpan={2}key={head.id}>{head.title}</TableHead>:
head.upName === 'upChangeRe2'&&head.upSequnce===1?<TableHead key={head.id} colSpan={3} style={{ whiteSpace: 'pre-line' }}>{'연간 운영실태'}</TableHead>:
head.upName === 'upChangeRe3'&&head.upSequnce===1?<TableHead key={head.id} colSpan={3} style={{ whiteSpace: 'pre-line' }}>{'하절기 운영실태\n(6~8월)'}</TableHead>:
head.upName === 'upChangeRe4'&&head.upSequnce===1?<TableHead key={head.id} colSpan={4} style={{ whiteSpace: 'pre-line' }}>{'동절기 운영실태\n(1~2월)'}</TableHead>:
head.upName === 'upChangeRe5'&&head.upSequnce===1?<TableHead key={head.id} colSpan={3} style={{ whiteSpace: 'pre-line' }}>{'지하수 유입량 추정'}</TableHead>:null))}
(head.upName === 'upChangeRe1'&&head.upSequnce===1?<TableHead className="fo-facility-info-header-surface" key={head.id} rowSpan={2}colSpan={2} style={{ whiteSpace: 'pre-line' }}>{head.title}</TableHead>:
!head.upSequnce?<TableHead className="fo-facility-info-header-surface" style={{ whiteSpace: 'pre-line' }} rowSpan={2}key={head.id}>{head.title}</TableHead>:
head.upName === 'upChangeRe2'&&head.upSequnce===1?<TableHead className="fo-facility-info-header-surface" key={head.id} colSpan={3} style={{ whiteSpace: 'pre-line' }}>{'연간 운영실태'}</TableHead>:
head.upName === 'upChangeRe3'&&head.upSequnce===1?<TableHead className="fo-facility-info-header-surface" key={head.id} colSpan={3} style={{ whiteSpace: 'pre-line' }}>{'하절기 운영실태\n(6~8월)'}</TableHead>:
head.upName === 'upChangeRe4'&&head.upSequnce===1?<TableHead className="fo-facility-info-header-surface" key={head.id} colSpan={4} style={{ whiteSpace: 'pre-line' }}>{'동절기 운영실태\n(1~2월)'}</TableHead>:
head.upName === 'upChangeRe5'&&head.upSequnce===1?<TableHead className="fo-facility-info-header-surface" key={head.id} colSpan={3} style={{ whiteSpace: 'pre-line' }}>{'지하수 유입량 추정'}</TableHead>:null))}
</TableRow>
<TableRow>
{upHeadList.map((head: TableUpperProps) =>
(head.upName === 'upChangeRe2'||head.upName === 'upChangeRe3'||head.upName === 'upChangeRe4'||head.upName === 'upChangeRe5'?<TableHead style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>:null))}
(head.upName === 'upChangeRe2'||head.upName === 'upChangeRe3'||head.upName === 'upChangeRe4'||head.upName === 'upChangeRe5'?<TableHead className="fo-facility-info-header-surface" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>:null))}
</TableRow>
</> )
}
@@ -239,18 +239,18 @@ export default function ProcessFacilitySearch({searchYear = '', fcltyList = []}:
:((index === 6 || index === 7 || index === 8) && (head.id === 'now' || head.id === 'stepOne' || head.id === 'stepThree' || head.id === 'stepFour'))?
null
:((index === 5) && (head.id === 'provisionalApproval' || head.id === 'stepTwo'))?
<TableCell colSpan={3} className="text-center border border-[var(--border)] bg-[rgb(234,243,251)] text-[rgb(100,119,132)]" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>
<TableCell colSpan={3} className="fo-facility-info-header-surface text-center" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>
:((index === 5) && (head.id === 'now' || head.id === 'stepOne' || head.id === 'stepThree'|| head.id === 'stepFour'))?
null
:(index < 5 || index > 5)&& (index!= 4) && (head.id ==='provisionalApproval'||head.id ==='now'||head.id ==='stepOne'||head.id ==='stepTwo'||head.id ==='stepThree'||head.id ==='stepFour')
?<TableCell className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>
:(head.id === "provisionalApproval" && index === 4)
?<TableCell colSpan={6} className="text-right border border-[var(--border)] text-[rgb(100,119,132)]" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{roundNumber(String((Number(waterUsage1)+Number(waterUsage2)+Number(waterUsage3))/3), 0)}</TableCell>
?<TableCell colSpan={6} className="text-right border border-border text-label" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{roundNumber(String((Number(waterUsage1)+Number(waterUsage2)+Number(waterUsage3))/3), 0)}</TableCell>
:((head.id ==='now'||head.id ==='stepOne'||head.id ==='stepTwo'||head.id ==='stepThree'||head.id ==='stepFour') )
?null
:(head.id === 'item' && index != 5)?
<TableCell className="text-center text-[rgb(100,119,132)]" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>
:<TableCell className="text-center border border-[var(--border)] bg-[rgb(234,243,251)] text-[rgb(100,119,132)]" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>
<TableCell className="text-center text-label" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>
:<TableCell className="fo-facility-info-header-surface text-center" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>
))}
</TableRow>
))}
@@ -278,15 +278,15 @@ export default function ProcessFacilitySearch({searchYear = '', fcltyList = []}:
(head.id === 'rdiRain'&&index === 0)?<TableCell rowSpan={7} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el['rdiRain']}</TableCell>:
(head.id === 'rdiRain'&&index === 7)?<TableCell rowSpan={6} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}></TableCell>:
(head.id === 'waterQlty')?<TableCell className="text-center" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>:
((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=0 && index<=4))?<TableCell className="bg-yellow-50 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=5 && index<=10))?<TableCell className="bg-green-50 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=11 && index<=12))?<TableCell className="bg-blue-50 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=0 && index<=4))?<TableCell className="bg-yellow-100 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=5 && index<=10))?<TableCell className="bg-green-100 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=11 && index<=12))?<TableCell className="bg-blue-100 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=0 && index<=4))?<TableCell className="bg-yellow-200 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=5 && index<=10))?<TableCell className="bg-green-200 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=11 && index<=12))?<TableCell className="bg-blue-200 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=0 && index<=4))?<TableCell className="fo-facility-info-flow-year text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=5 && index<=10))?<TableCell className="fo-facility-info-water-year text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=11 && index<=12))?<TableCell className="fo-facility-info-phosphorus-year text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=0 && index<=4))?<TableCell className="fo-facility-info-flow-summer text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=5 && index<=10))?<TableCell className="fo-facility-info-water-summer text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=11 && index<=12))?<TableCell className="fo-facility-info-phosphorus-summer text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=0 && index<=4))?<TableCell className="fo-facility-info-flow-winter text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=5 && index<=10))?<TableCell className="fo-facility-info-water-winter text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=11 && index<=12))?<TableCell className="fo-facility-info-phosphorus-winter text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
null
))}
</TableRow>
@@ -60,7 +60,14 @@ export default function DashBoardChart({searchYear = '', fcltyList = []}: facili
/**
* 차트 Config
*/
const colors = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#00b50c", "#D4A5A5"];
const colors = [
"var(--chart-5)",
"var(--chart-2)",
"var(--chart-1)",
"var(--chart-3)",
"var(--primary)",
"var(--chart-6)",
];
//유입하수량-강수량
const chartConfig_inflowRain = {
@@ -50,7 +50,7 @@ export default function RainManageTab({ searchYear = '', fcltyList = [] }: facil
widthFixed={true}
>
</UiTable>
<span className="text-2xl font-bold text-gray-500">☞ 일수에 강우영향일 포함됨</span>
<span className="text-2xl font-bold text-muted-foreground">☞ 일수에 강우영향일 포함됨</span>
</div>
)
}
@@ -1,5 +1,5 @@
/**
* 대시보드 - 표로보기 상단구역 페에지
* 대시보드 - 표로보기 상단구역 페이지
*
* - 대시보드 - 표로보기의 3개의 탭 상단에 조회년도, 행정정보, 시설명의 구역을 컴포넌트
*/
@@ -198,7 +198,7 @@ export default function PrecipitationModal(props: PrecipitationModalProps) {
mode="single"
selected={startDate}
onSelect={(date) => handleStartDateSelect(date as Date)}
className="w-full p-1 border rounded bg-white"
className="w-full rounded border border-input bg-form p-1 text-foreground"
disabled={(date) => !isDateInRanges(date)}
defaultMonth={startDate}
/>
@@ -209,7 +209,7 @@ export default function PrecipitationModal(props: PrecipitationModalProps) {
mode="single"
selected={endDate}
onSelect={(date) => handleEndDateSelect(date as Date)}
className="w-full p-1 border rounded bg-white"
className="w-full rounded border border-input bg-form p-1 text-foreground"
disabled={(date) => !isDateInRanges(date)}
defaultMonth={endDate}
/>
@@ -239,4 +239,4 @@ export default function PrecipitationModal(props: PrecipitationModalProps) {
</ListDiv>
</>
);
}
}
@@ -17,27 +17,27 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
return (
layoutNum === 0 ? (
<>
<TableRow className="bg-gray-50">
<TableRow className="bg-secondary">
{upHeadList.map((head: TableUpperProps) => {
if(!head.upName) {
return <TableHead className="bg-gray-50" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
return <TableHead className="bg-secondary" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
}else if(head.upName==="upChangeRe1"&&head.upSequnce===1) {
return <TableHead className="bg-gray-50" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>행정구역</TableHead>
return <TableHead className="bg-secondary" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>행정구역</TableHead>
}else if(head.upName==="upChangeRe3"&&head.upSequnce===1) {
return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
return <TableHead className="bg-secondary" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
}else if(head.upName==="upChangeRe4"&&head.upSequnce===1) {
return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유출</TableHead>
return <TableHead className="bg-secondary" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유출</TableHead>
}
})}
</TableRow>
<TableRow className="bg-gray-50">
<TableRow className="bg-secondary">
{upHeadList.map((head: TableUpperProps, idx: number) => {
if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4') {
if (idx == 1 || idx == 2 || idx == 3) {
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
}
else {
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
}
}
})}
@@ -45,27 +45,27 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
</>
) : layoutNum === 1 ? (
<>
<TableRow className="bg-gray-50">
<TableRow className="bg-secondary">
{upHeadList.map((head: TableUpperProps) => {
if(!head.upName) {
return <TableHead className="bg-gray-50" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
return <TableHead className="bg-secondary" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
}else if(head.upName==="upChangeRe1"&&head.upSequnce===1) {
return <TableHead className="bg-gray-50" colSpan={3} style={{ whiteSpace: 'pre-line' }} key={head.id}>권역</TableHead>
return <TableHead className="bg-secondary" colSpan={3} style={{ whiteSpace: 'pre-line' }} key={head.id}>권역</TableHead>
}else if(head.upName==="upChangeRe3"&&head.upSequnce===1) {
return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
return <TableHead className="bg-secondary" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
}else if(head.upName==="upChangeRe4"&&head.upSequnce===1) {
return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유출</TableHead>
return <TableHead className="bg-secondary" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유출</TableHead>
}
})}
</TableRow>
<TableRow className="bg-gray-50">
<TableRow className="bg-secondary">
{upHeadList.map((head: TableUpperProps, idx: number) => {
if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4') {
if (idx == 1 || idx == 2 || idx == 3) {
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
}
else {
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
}
}
})}
@@ -73,29 +73,29 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
</>
) : layoutNum === 2 ? (
<>
<TableRow className="bg-gray-50">
<TableRow className="bg-secondary">
{upHeadList.map((head: TableUpperProps) => {
if(!head.upName) {
return <TableHead className="bg-gray-50" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
return <TableHead className="bg-secondary" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
}else if(head.upName==="upChangeRe1"&&head.upSequnce===1) {
return <TableHead className="bg-gray-50" colSpan={3} style={{ whiteSpace: 'pre-line' }} key={head.id}>권역</TableHead>
return <TableHead className="bg-secondary" colSpan={3} style={{ whiteSpace: 'pre-line' }} key={head.id}>권역</TableHead>
}else if(head.upName==="upChangeRe2"&&head.upSequnce===1) {
return <TableHead className="bg-gray-50" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>행정구역</TableHead>
return <TableHead className="bg-secondary" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>행정구역</TableHead>
}else if(head.upName==="upChangeRe3"&&head.upSequnce===1) {
return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
return <TableHead className="bg-secondary" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
}else if(head.upName==="upChangeRe4"&&head.upSequnce===1) {
return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유출</TableHead>
return <TableHead className="bg-secondary" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유출</TableHead>
}
})}
</TableRow>
<TableRow className="bg-gray-50">
<TableRow className="bg-secondary">
{upHeadList.map((head: TableUpperProps, idx: number) => {
if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4') {
if (idx == 1 || idx == 2 || idx == 3) {
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
}
else {
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
}
}
})}
@@ -103,25 +103,25 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
</>
) : layoutNum === 3 ? (
<>
<TableRow className="bg-gray-50">
<TableRow className="bg-secondary">
{upHeadList.map((head: TableUpperProps) => {
if(!head.upName) {
return <TableHead className="bg-gray-50" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
return <TableHead className="bg-secondary" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
}else if(head.upName==="upChangeRe1"&&head.upSequnce===1) {
return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
return <TableHead className="bg-secondary" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
}else if(head.upName==="upChangeRe2"&&head.upSequnce===1) {
return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>방류</TableHead>
return <TableHead className="bg-secondary" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>방류</TableHead>
}
})}
</TableRow>
<TableRow className="bg-gray-50">
<TableRow className="bg-secondary">
{upHeadList.map((head: TableUpperProps, idx: number) => {
if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2') {
if (idx == 1 || idx == 2 || idx == 3) {
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
}
else {
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
}
}
})}
@@ -129,23 +129,23 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
</>
) : layoutNum === 4 ? (
<>
<TableRow className="bg-gray-50">
<TableRow className="bg-secondary">
{upHeadList.map((head: TableUpperProps) => {
if(!head.upName) {
return <TableHead className="bg-gray-50" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
return <TableHead className="bg-secondary" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
}else if(head.upName==="upChangeRe"&&head.upSequnce===1) {
return <TableHead className="bg-gray-50" colSpan={4} style={{ whiteSpace: 'pre-line' }} key={head.id}>방류</TableHead>
return <TableHead className="bg-secondary" colSpan={4} style={{ whiteSpace: 'pre-line' }} key={head.id}>방류</TableHead>
}
})}
</TableRow>
<TableRow className="bg-gray-50">
<TableRow className="bg-secondary">
{upHeadList.map((head: TableUpperProps, idx: number) => {
if (head.upName === 'upChangeRe') {
if (idx == 1 || idx == 2 || idx == 3) {
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
}
else {
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
}
}
})}
@@ -153,21 +153,21 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
</>
) : layoutNum === 5 ? ( // 총인설비 수질현황 헤더
<>
<TableRow className="bg-gray-50">
<TableRow className="bg-secondary">
{upHeadList.map((head: TableUpperProps) => {
if(!head.upName) {
return <TableHead className="w-[180px] bg-gray-50" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
return <TableHead className="w-[180px] bg-secondary" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
}else if(head.upName==="upChangeRe1" && head.upSequnce===1) {
return <TableHead className="bg-gray-50" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
return <TableHead className="bg-secondary" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
}else if(head.upName==="upChangeRe2" && head.upSequnce===1) {
return <TableHead className="bg-gray-50" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>방류</TableHead>
return <TableHead className="bg-secondary" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>방류</TableHead>
}
})}
</TableRow>
<TableRow className="bg-gray-50">
<TableRow className="bg-secondary">
{upHeadList.map((head: TableUpperProps) => {
if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2') {
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
}
})}
</TableRow>
@@ -237,7 +237,7 @@ export default function FlowRateModal(props: { fcltyName: string; facilityCd: st
const raw = row.infoRnDay;
const value = typeof raw === "string" ? Number(raw.replace(/,/g, "").trim()) : Number(raw);
if (!Number.isFinite(value)) return "";
return value > 3 ? "bg-slate-50" : "";
return value > 3 ? "fo-rain-impact-row" : "";
},
[activeTab],
);
@@ -5,77 +5,77 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
return (
layoutNum === 0 ? (
<>
<TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) =>
<TableRow className="bg-secondary">{upHeadList.map((head: TableUpperProps) =>
(head.id === 'trtFrfNm' ? <TableHead className="w-1/12" rowSpan={3} key={head.id}>{head.title}</TableHead> :
head.id === 'laeNm' ? <TableHead className="w-1/12" rowSpan={3} key={head.id}>{head.title}</TableHead> :
head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? <TableHead rowSpan={3} className="w-40" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> :
head.id === 'trstOprEtpNm' ? <TableHead className="w-1/15" rowSpan={3} key={head.id}>{head.title}</TableHead> :
head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={2}>{'행정구역'}</TableHead> :
head.upName === 'upChangeRe3' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={6}>{'증설계획'}</TableHead> :
head.upName === 'upChangeRe4' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={19}>{'시설계획'}</TableHead> :
!head.upName ? <TableHead rowSpan={3} className="w-1/20 bg-gray-20" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={2}>{'행정구역'}</TableHead> :
head.upName === 'upChangeRe3' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={6}>{'증설계획'}</TableHead> :
head.upName === 'upChangeRe4' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={19}>{'시설계획'}</TableHead> :
!head.upName ? <TableHead rowSpan={3} className="w-1/20 bg-accent text-accent-foreground" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
</TableRow>
<TableRow className="bg-gray-50">
<TableRow className="bg-secondary">
{upHeadList.map((head: TableUpperProps) => (
head.upName === 'upChangeRe4' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'증설계획(m³/일)'}</TableHead> :
head.upName === 'upChangeRe5' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'처리인구(인)'}</TableHead> :
head.upName === 'upChangeRe6' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={7}>{'처리구역(k㎡)'}</TableHead> :
head.upName === 'upChangeRe4' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'증설계획(m³/일)'}</TableHead> :
head.upName === 'upChangeRe5' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'처리인구(인)'}</TableHead> :
head.upName === 'upChangeRe6' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={7}>{'처리구역(k㎡)'}</TableHead> :
null
))}
</TableRow>
<TableRow className="bg-gray-50">
<TableRow className="bg-secondary">
{upHeadList.map((head: TableUpperProps) => {
if (head.upName === 'upChangeRe' || head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4' || head.upName === 'upChangeRe5' || head.upName === 'upChangeRe6') {
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>;
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-secondary">{head.title}</TableHead>;
}
})}
</TableRow>
</>
) : layoutNum === 1 ? (
<>
<TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) =>
<TableRow className="bg-secondary">{upHeadList.map((head: TableUpperProps) =>
(head.id === 'trtFrfNm' ? <TableHead className="w-1/12" rowSpan={3} key={head.id}>{head.title}</TableHead> :
head.id === 'laeNm' ? <TableHead className="w-1/12" rowSpan={3} key={head.id}>{head.title}</TableHead> :
head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? <TableHead rowSpan={3} className="w-40" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> :
head.id === 'trstOprEtpNm' ? <TableHead className="w-1/15" rowSpan={3} key={head.id}>{head.title}</TableHead> :
head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={3}>{'유역'}</TableHead> :
head.upName === 'upChangeRe3' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={6}>{'증설계획'}</TableHead> :
!head.upName ? <TableHead rowSpan={3} className="w-1/20 bg-gray-20" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={3}>{'유역'}</TableHead> :
head.upName === 'upChangeRe3' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={6}>{'증설계획'}</TableHead> :
!head.upName ? <TableHead rowSpan={3} className="w-1/20 bg-accent text-accent-foreground" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
</TableRow>
<TableRow className="bg-gray-50">
<TableRow className="bg-secondary">
{upHeadList.map((head: TableUpperProps) => (
head.upName === 'upChangeRe4' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'증설계획(m³/일)'}</TableHead> :
head.upName === 'upChangeRe5' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'처리인구(인)'}</TableHead> :
head.upName === 'upChangeRe6' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={7}>{'처리구역(k㎡)'}</TableHead> :
head.upName === 'upChangeRe4' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'증설계획(m³/일)'}</TableHead> :
head.upName === 'upChangeRe5' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'처리인구(인)'}</TableHead> :
head.upName === 'upChangeRe6' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={7}>{'처리구역(k㎡)'}</TableHead> :
null
))}
</TableRow>
<TableRow className="bg-gray-50">
<TableRow className="bg-secondary">
{upHeadList.map((head: TableUpperProps) => {
if (head.upName === 'upChangeRe' || head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4' || head.upName === 'upChangeRe5' || head.upName === 'upChangeRe6') {
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>;
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-secondary">{head.title}</TableHead>;
}
})}
</TableRow>
</>
) : (
<>
<TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) =>
<TableRow className="bg-secondary">{upHeadList.map((head: TableUpperProps) =>
(head.id === 'trtFrfNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> :
head.id === 'laeNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> :
head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? <TableHead rowSpan={2} className="w-40" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> :
head.id === 'trstOprEtpNm' ? <TableHead className="w-1/15" rowSpan={2} key={head.id}>{head.title}</TableHead> :
head.upName === 'upChangeRe1' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={3}>{'권역'}</TableHead> :
head.upName === 'upChangeRe2' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={2}>{'환경청'}</TableHead> :
head.upName === 'upChangeRe3' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'증설계획(m³/일)'}</TableHead> :
head.upName === 'upChangeRe5' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'처리인구(인)'}</TableHead> :
head.upName === 'upChangeRe6' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={7}>{'처리구역(k㎡)'}</TableHead> :
!head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-gray-20" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
head.upName === 'upChangeRe1' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={3}>{'권역'}</TableHead> :
head.upName === 'upChangeRe2' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={2}>{'환경청'}</TableHead> :
head.upName === 'upChangeRe3' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'증설계획(m³/일)'}</TableHead> :
head.upName === 'upChangeRe5' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'처리인구(인)'}</TableHead> :
head.upName === 'upChangeRe6' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={7}>{'처리구역(k㎡)'}</TableHead> :
!head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-accent text-accent-foreground" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
</TableRow>
<TableRow className="bg-gray-50">
<TableRow className="bg-secondary">
{upHeadList.map((head: TableUpperProps) => {
if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2' ||head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4' || head.upName === 'upChangeRe5' || head.upName === 'upChangeRe6') {
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>;
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-secondary">{head.title}</TableHead>;
}
})}
</TableRow>
@@ -84,4 +84,4 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
);
}
export { HeadMakeColSpan };
export { HeadMakeColSpan };
@@ -116,7 +116,7 @@ export function SearchBar({ onFeatureSelect, gisMaps }: SearchBarProps) {
<SelectBox
labelClass="w-[40px]"
className="mr-10 w-[200px]"
selectClass="rounded-lg bg-white"
selectClass="rounded-lg"
label="시도"
selectArray={sidoData}
onSelectValue={(val) => onSelectValue(val.toString(), "ctpvCd")}
@@ -125,7 +125,7 @@ export function SearchBar({ onFeatureSelect, gisMaps }: SearchBarProps) {
<SelectBox
labelClass="w-[65px]"
className="mr-10 w-[200px]"
selectClass="rounded-lg bg-white"
selectClass="rounded-lg"
label="시군구"
selectArray={filteredSigunData}
onSelectValue={(val) => onSelectValue(val.toString(), "sggCd")}
@@ -134,7 +134,7 @@ export function SearchBar({ onFeatureSelect, gisMaps }: SearchBarProps) {
<SelectBox
labelClass="w-[140px]"
className="mr-3 w-[250px]"
selectClass="rounded-lg bg-white"
selectClass="rounded-lg"
label="하수처리시설"
selectArray={facilityData}
onSelectValue={(val) => onSelectValue(val.toString(), "fcltCd")}
@@ -0,0 +1,164 @@
# apps/fo 테마 적용 작업계획
작성일: 2026-06-25
대상 앱: `D:\iipbs\frontend\apps\fo`
## 1. 목적
- `apps/fo` 프로그램 페이지 내부에 직접 박혀 있는 색상 하드코딩을 식별한다.
- 가능한 범위는 `packages/common/assets/src/styles/base.css`의 CSS 변수 영향권으로 옮긴다.
- 즉 `bg-white`, `text-gray-*`, `border-gray-*`, `bg-blue-*`, `bg-green-*`, `bg-red-*` 같은 직접 색상 표현을 `background`, `foreground`, `label`, `border`, `secondary`, `accent`, `primary`, `destructive` 계열 토큰으로 치환한다.
## 2. 전제
- `apps/fo`는 [tailwind.config.ts](D:/iipbs/frontend/apps/fo/tailwind.config.ts:1)에서 공통 preset을 사용한다.
- 공통 토큰은 [packages/common/assets/src/styles/base.css](D:/iipbs/frontend/packages/common/assets/src/styles/base.css:1)에 정의되어 있다.
- `packages/common/components`, `packages/common/assets`는 기준 레이어이며, 앱 작업은 이 기준을 따르는 방향으로 진행한다.
- 단, 이번 요청의 1차 목표는 `apps/fo` 내부 코드에 박혀 있는 색상을 줄이는 것이다.
## 3. 확인된 하드코딩 유형
현재 `apps/fo`에서 확인된 대표 패턴은 아래와 같다.
- 검색 입력/선택 UI
- `apps/fo/app/components/common/SearchInput.tsx`
- `apps/fo/app/pages/map/searchBar/searchBar.tsx`
- `apps/fo/app/components/common/FacilityTermModalHeader.tsx`
- `apps/fo/app/components/common/FacilityTermModalHeaderMonth.tsx`
- 대시보드/안내문구
- `apps/fo/app/components/common/UnderComment.tsx`
- `apps/fo/app/pages/(with-layout)/dashboard/rainManage/page.tsx`
- 대시보드 표 셀 상태색
- `apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/page.tsx`
이 페이지는 특별한 페이지로, 연간, 하절기, 동절기 색을 구분하여 보여주어야 한다. 따라서 text-foreground 가 보이도록 하되 연간, 하절기, 동절기 색을 apps\fo\src\app\styles 아래 테마별 css에 정의해서 보여주어야 한다.
- 지도 오버레이/팝업
- `apps/fo/app/pages/(map-layout)/map/page.tsx`
- 테스트/샘플성 버튼
- `apps/fo/app/pages/(with-layout)/test/page.tsx`
- 모달 입력창
- `apps/fo/app/pages/(with-layout)/facilityPrecipitation/modal/page.tsx`
## 4. 기본 원칙
### 4.1 바로 토큰 치환 가능한 경우
아래는 우선 토큰으로 바로 바꾼다.
- `bg-white` -> `bg-background` 또는 `bg-card` 또는 `bg-popover`
- `text-gray-900`, `text-gray-800` -> `text-foreground`
- `text-gray-700`, `text-gray-600` -> `text-label` 또는 `text-foreground`
- `text-gray-500` -> `text-muted-foreground` 또는 `text-placeholder`
- `border-gray-200`, `border-gray-300` -> `border-border`
- `focus:ring-blue-500` -> `focus:ring-ring`
- `text-blue-500`, `text-blue-600` -> `text-primary`
- `bg-blue-500`, `bg-blue-600` -> `bg-primary`
- `text-red-500`, `text-red-600` -> `text-destructive`
- `bg-red-50` 등 경고 계열 -> `bg-accent` 또는 별도 위험 상태 규칙 검토
### 4.2 바로 토큰 치환하면 안 되는 경우
아래는 의미색일 가능성이 높아 바로 토큰화하지 않는다.
- 범례 색상
- 데이터 구분용 heatmap/구간 색상
- 수질/계절/유형별 비교 색상
- 지도 오버레이에서 정보 카테고리 의미를 나타내는 색
이 경우는 다음 중 하나로 처리한다.
- 유지
- 별도 의미 클래스 부여
- 앱 전용 CSS로 상태 클래스를 만든 뒤 theme-safe surface만 토큰화
## 5. 우선순위
### 5.1 1순위: 일반 UI surface
가장 먼저 처리한다.
- 검색 입력창 배경/보더/텍스트
- 검색 결과 dropdown 배경/hover
- 팝업/모달 wrapper 배경/보더
- 제목/라벨/안내문구 색
- 버튼의 직접 blue/red/white 색상
이 영역은 theme 적용 체감이 가장 크고, 토큰 치환도 안전하다.
### 5.2 2순위: 테이블/리스트
- `selectedFcltList`
- `facilityInfo`
- modal 내부 표
- overlay 내 표 구조
이 영역은 `bg-secondary`, `bg-accent`, `text-secondary-foreground`, `border-border` 계열로 정리한다.
### 5.3 3순위: 의미색 데이터 표현
- `facilityInfo`의 계절/항목별 `bg-green-*`, `bg-blue-*`
- 지도 범례 색상
- 지도 feature overlay 강조색
- 대시보드 표 셀 상태색
- `apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/page.tsx`
이 페이지는 특별한 페이지로, 연간, 하절기, 동절기 색을 구분하여 보여주어야 한다. 따라서 text-foreground 가 보이도록 하되 연간, 하절기, 동절기 색을 apps\fo\src\app\styles 아래 테마별 css에 정의해서 보여주어야 한다.
이 영역은 “없애는 것”보다 “역할을 분리하는 것”이 중요하다.
## 6. 실제 작업 순서
1. `apps/fo` 내부 하드코딩 색상 파일 목록 확정
2. 공통 UI와 데이터 의미색을 분리
3. 일반 UI 색상부터 `base.css` 토큰으로 치환
4. 필요 시 `apps/fo/app/styles/*.css`에 앱 전용 의미 클래스 추가
5. `dashboard`, `map`, `modal`, `search` 대표 화면 점검
6. 남은 의미색 영역은 별도 상태색 정책으로 정리
## 7. 권장 치환 전략
### 7.1 JSX 안에서 바로 치환
아래처럼 className을 직접 토큰 기반으로 변경한다.
- `bg-white` -> `bg-background`
- `text-gray-600` -> `text-label`
- `border-gray-300` -> `border-border`
이 방식은 빠르지만 반복이 많으면 유지보수성이 떨어진다.
### 7.2 반복되는 UI는 앱 전용 클래스 도입
예:
- `fo-search-panel`
- `fo-search-input`
- `fo-popup-card`
- `fo-empty-text`
반복되는 구조는 `apps/fo/app/styles/components*.css`에 올리고, JSX에서는 의미 클래스만 사용한다.
이 방식이 장기적으로 더 좋다.
## 8. 예상 주의사항
- `h-90` 같은 Tailwind v3 미지원 클래스가 섞여 있을 수 있다.
- `bg-white`를 `bg-background`로 바꾸면 black theme에서 contrast가 크게 달라질 수 있으므로 텍스트 색도 같이 바꿔야 한다.
- `bg-blue-*`, `bg-green-*`는 실제 status color인지 단순 강조색인지 구분하고 바꿔야 한다.
- 지도 오버레이는 HTML string으로 만드는 부분이 있으면 className 치환만으로 끝나지 않을 수 있다.
## 9. 1차 적용 추천 대상
우선 아래 파일부터 적용하는 것이 효율적이다.
1. `apps/fo/app/components/common/SearchInput.tsx`
2. `apps/fo/app/pages/map/searchBar/searchBar.tsx`
3. `apps/fo/app/components/common/UnderComment.tsx`
4. `apps/fo/app/pages/(with-layout)/dashboard/rainManage/page.tsx`
5. `apps/fo/app/components/common/FacilityTermModalHeader.tsx`
6. `apps/fo/app/components/common/FacilityTermModalHeaderMonth.tsx`
7. `apps/fo/app/pages/(with-layout)/facilityPrecipitation/modal/page.tsx`
## 10. 결론
- `apps/fo`도 `apps/wr`와 동일하게 `base.css` 토큰 중심으로 정리 가능하다.
- 1차는 일반 UI 하드코딩 제거에 집중하고, 데이터 의미색은 유지 또는 별도 정책으로 분리하는 것이 맞다.
- 가장 안전한 방향은 “반복 UI는 앱 전용 의미 클래스”, “일회성은 JSX에서 직접 토큰 치환”의 혼합 방식이다.
+1
View File
@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

+4
View File
@@ -1,5 +1,9 @@
import "./styles/base.css";
import "./styles/components.css";
import "./styles/components-black.css";
import "./styles/components-blue.css";
import "./styles/components-green.css";
import "./styles/components-red.css";
import "@fontsource-variable/noto-sans-kr";
import { AppProvider } from "./providers";
@@ -0,0 +1,43 @@
@tailwind components;
@layer components {
.card {
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
}
.card-border {
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
}
.card-header {
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
}
.card-footer {
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
}
[data-theme="black"] {
--fo-facility-info-flow-year-bg: color-mix(in srgb, var(--chart-4) 20%, var(--secondary));
--fo-facility-info-water-year-bg: color-mix(in srgb, var(--chart-3) 20%, var(--secondary));
--fo-facility-info-phosphorus-year-bg: color-mix(in srgb, var(--chart-1) 20%, var(--secondary));
--fo-facility-info-flow-summer-bg: color-mix(in srgb, var(--chart-4) 28%, var(--secondary));
--fo-facility-info-water-summer-bg: color-mix(in srgb, var(--chart-3) 28%, var(--secondary));
--fo-facility-info-phosphorus-summer-bg: color-mix(in srgb, var(--chart-1) 28%, var(--secondary));
--fo-facility-info-flow-winter-bg: color-mix(in srgb, var(--chart-4) 36%, var(--secondary));
--fo-facility-info-water-winter-bg: color-mix(in srgb, var(--chart-3) 36%, var(--secondary));
--fo-facility-info-phosphorus-winter-bg: color-mix(in srgb, var(--chart-1) 36%, var(--secondary));
--fo-rain-impact-row-bg: color-mix(in srgb, var(--chart-1) 16%, var(--secondary));
--fo-facility-info-header-bg: #5e7b9c;
--fo-facility-info-header-fg: #ffffff;
--fo-facility-info-header-border: var(--border);
.card-border {
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
}
.card-header {
@apply border-b-[#3f4042];
}
}
input[type="file"] {
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
}
}
@@ -0,0 +1,95 @@
@tailwind components;
@layer components {
.card {
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
}
.card-border {
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
}
.card-header {
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
}
.card-footer {
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
}
[data-theme="blue"] {
--fo-facility-info-flow-year-bg: color-mix(in srgb, var(--chart-4) 16%, var(--background));
--fo-facility-info-water-year-bg: color-mix(in srgb, var(--chart-3) 16%, var(--background));
--fo-facility-info-phosphorus-year-bg: color-mix(in srgb, var(--chart-1) 16%, var(--background));
--fo-facility-info-flow-summer-bg: color-mix(in srgb, var(--chart-4) 24%, var(--background));
--fo-facility-info-water-summer-bg: color-mix(in srgb, var(--chart-3) 24%, var(--background));
--fo-facility-info-phosphorus-summer-bg: color-mix(in srgb, var(--chart-1) 24%, var(--background));
--fo-facility-info-flow-winter-bg: color-mix(in srgb, var(--chart-4) 32%, var(--background));
--fo-facility-info-water-winter-bg: color-mix(in srgb, var(--chart-3) 32%, var(--background));
--fo-facility-info-phosphorus-winter-bg: color-mix(in srgb, var(--chart-1) 32%, var(--background));
--fo-rain-impact-row-bg: color-mix(in srgb, var(--chart-1) 12%, var(--background));
--fo-facility-info-header-bg: #eaf3fb;
--fo-facility-info-header-fg: #647784;
--fo-facility-info-header-border: #e5e5e5;
.card-border {
@apply border-[#d8e3eb] bg-white shadow-blur;
}
.card-header {
@apply border-b-[#d8e3eb];
}
.card {
@apply text-[#4b5563];
}
.card.card-border {
box-shadow: 0 1px 8px rgba(15, 23, 42, 0.06);
}
.card.card-border .card.card-border {
@apply border-[#dde6ed] bg-white;
box-shadow: none;
}
.card.card-border input,
.card.card-border button[role="combobox"] {
@apply border-[#d8e3eb] bg-white text-[#737373];
}
.card.card-border label {
@apply text-[#666666];
}
nav[aria-label="pagination"] li a {
border-color: transparent !important;
background-color: transparent !important;
color: #0a0a0a !important;
}
nav[aria-label="pagination"] li a:hover {
background-color: #f6f6f6 !important;
color: #0a0a0a !important;
}
nav[aria-label="pagination"] li a[aria-current="page"] {
border-color: #2094fa !important;
background-color: #2094fa !important;
color: #fafafa !important;
}
nav[aria-label="pagination"] li a[aria-label="Go to previous page"],
nav[aria-label="pagination"] li a[aria-label="Go to next page"] {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
border: 1px solid #e5e7eb !important;
color: #666666 !important;
}
}
input[type="file"] {
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
}
}
@@ -0,0 +1,43 @@
@tailwind components;
@layer components {
.card {
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
}
.card-border {
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
}
.card-header {
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
}
.card-footer {
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
}
[data-theme="green"] {
--fo-facility-info-flow-year-bg: color-mix(in srgb, var(--chart-4) 15%, var(--background));
--fo-facility-info-water-year-bg: color-mix(in srgb, var(--chart-3) 15%, var(--background));
--fo-facility-info-phosphorus-year-bg: color-mix(in srgb, var(--chart-1) 15%, var(--background));
--fo-facility-info-flow-summer-bg: color-mix(in srgb, var(--chart-4) 23%, var(--background));
--fo-facility-info-water-summer-bg: color-mix(in srgb, var(--chart-3) 23%, var(--background));
--fo-facility-info-phosphorus-summer-bg: color-mix(in srgb, var(--chart-1) 23%, var(--background));
--fo-facility-info-flow-winter-bg: color-mix(in srgb, var(--chart-4) 31%, var(--background));
--fo-facility-info-water-winter-bg: color-mix(in srgb, var(--chart-3) 31%, var(--background));
--fo-facility-info-phosphorus-winter-bg: color-mix(in srgb, var(--chart-1) 31%, var(--background));
--fo-rain-impact-row-bg: color-mix(in srgb, var(--chart-3) 12%, var(--background));
--fo-facility-info-header-bg: #ebf7f7;
--fo-facility-info-header-fg: #64908f;
--fo-facility-info-header-border: var(--border);
.card-border {
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
}
.card-header {
@apply border-b-[#3f4042];
}
}
input[type="file"] {
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
}
}
+36
View File
@@ -0,0 +1,36 @@
@tailwind components;
@layer components {
.card {
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
}
.card-border {
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
}
.card-header {
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
}
.card-footer {
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
}
[data-theme="red"] {
--fo-facility-info-flow-year-bg: color-mix(in srgb, var(--chart-4) 14%, var(--background));
--fo-facility-info-water-year-bg: color-mix(in srgb, var(--chart-3) 14%, var(--background));
--fo-facility-info-phosphorus-year-bg: color-mix(in srgb, var(--chart-1) 14%, var(--background));
--fo-facility-info-flow-summer-bg: color-mix(in srgb, var(--chart-4) 22%, var(--background));
--fo-facility-info-water-summer-bg: color-mix(in srgb, var(--chart-3) 22%, var(--background));
--fo-facility-info-phosphorus-summer-bg: color-mix(in srgb, var(--chart-1) 22%, var(--background));
--fo-facility-info-flow-winter-bg: color-mix(in srgb, var(--chart-4) 30%, var(--background));
--fo-facility-info-water-winter-bg: color-mix(in srgb, var(--chart-3) 30%, var(--background));
--fo-facility-info-phosphorus-winter-bg: color-mix(in srgb, var(--chart-1) 30%, var(--background));
--fo-rain-impact-row-bg: color-mix(in srgb, var(--destructive) 12%, var(--background));
--fo-facility-info-header-bg: #f6f6f9;
--fo-facility-info-header-fg: #808080;
--fo-facility-info-header-border: var(--border);
}
input[type="file"] {
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
}
}
+73 -7
View File
@@ -1,6 +1,22 @@
@tailwind components;
@layer components {
:root {
--fo-facility-info-flow-year-bg: color-mix(in srgb, var(--chart-4) 18%, var(--background));
--fo-facility-info-water-year-bg: color-mix(in srgb, var(--chart-3) 18%, var(--background));
--fo-facility-info-phosphorus-year-bg: color-mix(in srgb, var(--chart-1) 18%, var(--background));
--fo-facility-info-flow-summer-bg: color-mix(in srgb, var(--chart-4) 28%, var(--background));
--fo-facility-info-water-summer-bg: color-mix(in srgb, var(--chart-3) 28%, var(--background));
--fo-facility-info-phosphorus-summer-bg: color-mix(in srgb, var(--chart-1) 28%, var(--background));
--fo-facility-info-flow-winter-bg: color-mix(in srgb, var(--chart-4) 38%, var(--background));
--fo-facility-info-water-winter-bg: color-mix(in srgb, var(--chart-3) 38%, var(--background));
--fo-facility-info-phosphorus-winter-bg: color-mix(in srgb, var(--chart-1) 38%, var(--background));
--fo-rain-impact-row-bg: color-mix(in srgb, var(--chart-1) 10%, var(--background));
--fo-facility-info-header-bg: #eaf3fb;
--fo-facility-info-header-fg: #647784;
--fo-facility-info-header-border: var(--border);
}
.card {
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
}
@@ -14,13 +30,63 @@
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
}
[data-theme="black"] {
.card-border {
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
}
.card-header {
@apply border-b-[#3f4042];
}
.fo-facility-info-flow-year,
.fo-facility-info-water-year,
.fo-facility-info-phosphorus-year,
.fo-facility-info-flow-summer,
.fo-facility-info-water-summer,
.fo-facility-info-phosphorus-summer,
.fo-facility-info-flow-winter,
.fo-facility-info-water-winter,
.fo-facility-info-phosphorus-winter {
color: var(--foreground);
}
.fo-facility-info-flow-year {
background-color: var(--fo-facility-info-flow-year-bg);
}
.fo-facility-info-water-year {
background-color: var(--fo-facility-info-water-year-bg);
}
.fo-facility-info-phosphorus-year {
background-color: var(--fo-facility-info-phosphorus-year-bg);
}
.fo-facility-info-flow-summer {
background-color: var(--fo-facility-info-flow-summer-bg);
}
.fo-facility-info-water-summer {
background-color: var(--fo-facility-info-water-summer-bg);
}
.fo-facility-info-phosphorus-summer {
background-color: var(--fo-facility-info-phosphorus-summer-bg);
}
.fo-facility-info-flow-winter {
background-color: var(--fo-facility-info-flow-winter-bg);
}
.fo-facility-info-water-winter {
background-color: var(--fo-facility-info-water-winter-bg);
}
.fo-facility-info-phosphorus-winter {
background-color: var(--fo-facility-info-phosphorus-winter-bg);
}
.fo-rain-impact-row td {
background-color: var(--fo-rain-impact-row-bg);
}
.fo-facility-info-header-surface {
border: 1px solid var(--fo-facility-info-header-border);
background-color: var(--fo-facility-info-header-bg);
color: var(--fo-facility-info-header-fg);
}
input[type="file"] {
+1
View File
@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

+6
View File
@@ -1,3 +1,9 @@
import "./styles/base.css";
import "./styles/components.css";
import "./styles/components-black.css";
import "./styles/components-blue.css";
import "./styles/components-green.css";
import "./styles/components-red.css";
import { AppProvider } from "./providers";
import { cookies } from "next/headers";
@@ -0,0 +1,29 @@
@tailwind components;
@layer components {
.card {
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
}
.card-border {
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
}
.card-header {
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
}
.card-footer {
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
}
[data-theme="black"] {
.card-border {
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
}
.card-header {
@apply border-b-[#3f4042];
}
}
input[type="file"] {
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
}
}
@@ -0,0 +1,29 @@
@tailwind components;
@layer components {
.card {
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
}
.card-border {
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
}
.card-header {
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
}
.card-footer {
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
}
[data-theme="blue"] {
.card-border {
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
}
.card-header {
@apply border-b-[#3f4042];
}
}
input[type="file"] {
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
}
}
@@ -0,0 +1,29 @@
@tailwind components;
@layer components {
.card {
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
}
.card-border {
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
}
.card-header {
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
}
.card-footer {
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
}
[data-theme="green"] {
.card-border {
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
}
.card-header {
@apply border-b-[#3f4042];
}
}
input[type="file"] {
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
}
}
+29
View File
@@ -0,0 +1,29 @@
@tailwind components;
@layer components {
.card {
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
}
.card-border {
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
}
.card-header {
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
}
.card-footer {
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
}
[data-theme="red"] {
.card-border {
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
}
.card-header {
@apply border-b-[#3f4042];
}
}
input[type="file"] {
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
}
}
-9
View File
@@ -14,15 +14,6 @@
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
}
[data-theme="black"] {
.card-border {
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
}
.card-header {
@apply border-b-[#3f4042];
}
}
input[type="file"] {
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
}
+2 -1
View File
@@ -4,4 +4,5 @@ NEXT_PUBLIC_PROXY_HOST=http://localhost:3001
SERVER_API_URL=http://192.168.0.102:8000
NEXT_PUBLIC_SITE_ID=ST
NEXT_PUBLIC_SUB_DOMAIN=http://localhost:3000
NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2
NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2
NEXT_PUBLIC_ST_DASHBOARD_BBS_ID=13
+20 -170
View File
@@ -1,54 +1,32 @@
"use client";
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
import { usePathname } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
import {
getTabStoreState as getGridTabStoreState,
useTabStore as gridTabStore,
} from "@rsone/components";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib";
import { isBbsNoticeMenu } from "@/shared/lib/bbs-notice-menu";
import {
menuMatchesStatisticsTabId,
resolveStatisticsTabId,
} from "@/shared/lib/statistics-menu-url";
import { useStatisticsTabUrlSync } from "@/shared/lib/use-statistics-tab-url-sync";
import { isVerificationSelectionReportMenu } from "@/shared/lib/verification-selection-report";
import { StatisticsPageLayout } from "@/shared/ui/statistics-page-layout";
import { tokenManager } from "@/entities/auth";
import type { MenuItem } from "@/entities/menu";
import { MenuApi } from "@/entities/menu";
import { BaseLayout } from "@common/components";
import { getConfig } from "@common/config";
import { getTabStoreState, useTabStore } from "@/store/tab-store";
import VerificationSelectionReport from "@/components/statistics/VerificationSelectionReport";
import { getTabStoreState, useTabStore } from "@/store/tab-store";
const appConfig = getConfig();
@@ -58,42 +36,23 @@ const appSiteId = getCookie("site-id-key") ?? "";
const isLoggedIn = !!userId;
function WithBaseLayoutInner({
children,
}: {
children: React.ReactNode;
}) {
function WithBaseLayoutInner({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const router = useRouter();
const queryClient = useQueryClient();
/** 탭 추가 없이 메뉴만 눌렀을 때 패널 유지 (다른 메뉴 클릭해도 유지) */
const [isVerificationSelectionReportPanelOpen, setReportPanelOpen] =
useState(false);
const setCurrentMenuInfo = useMenuStore(
(state) => state.setCurrentMenuInfo,
);
const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo);
const addTab = useTabStore((state) => state.addTab);
const setActiveTab = useTabStore((state) => state.setActiveTab);
@@ -108,16 +67,11 @@ function WithBaseLayoutInner({
const addTabForGrid = gridTabStore((state) => state.addTab);
const setActiveTabForGrid = gridTabStore((state) => state.setActiveTab);
const setMenuListForGrid = gridTabStore((state) => state.setMenuList);
const alert = useAlertStore((state) => state.setMessage);
const { syncTabToUrl } = useStatisticsTabUrlSync({
pathname,
menuList,
@@ -129,41 +83,27 @@ function WithBaseLayoutInner({
addTabForGrid,
setCurrentMenuInfo,
});
const { data: menuListData } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
queryFn: () => MenuApi.getMenuRoles(appSiteId),
enabled: isLoggedIn,
});
useEffect(() => {
if (menuListData) {
setMenuList(menuListData);
setMenuListForGrid(menuListData);
}
}, [menuListData, setMenuList, setMenuListForGrid]);
// 새로고침 후 persist로 앱 탭만 복원된 경우 rsone 그리드 스토어에 탭을 맞춤
useEffect(() => {
if (!menuList) return;
const appTabs = getTabStoreState().tabs;
@@ -172,20 +112,13 @@ function WithBaseLayoutInner({
if (getGridTabStoreState().tabs.length > 0) return;
for (const tab of appTabs) {
getGridTabStoreState().addTab(tab.menuItem);
}
const activeId = getTabStoreState().activeTabId;
if (activeId) {
setActiveTab(activeId);
const activeTab = appTabs.find((t) => t.id === activeId);
@@ -193,155 +126,104 @@ function WithBaseLayoutInner({
const gridActiveId = activeTab?.menuItem.menuEngNm;
if (gridActiveId) {
getGridTabStoreState().setActiveTab(gridActiveId);
}
}
}, [menuList, tabs, setActiveTab]);
// 활성 탭의 메뉴를 찾아서 urlPath를 가져옴. 탭이 전부 닫히면 ''를 넘겨 LNB에서 메뉴 강조 제거
const currentPath = useMemo(() => {
if (tabs.length === 0) return "";
if (activeTabId && menuList) {
// menuList에서 활성 탭의 메뉴 찾기
const findMenuByEngNm = (items: MenuItem[]): MenuItem | null => {
for (const item of items) {
if (item.urlPath === activeTabId) {
if (menuMatchesStatisticsTabId(item, activeTabId)) {
return item;
}
if (item.children) {
const found = findMenuByEngNm(item.children);
if (found) return found;
}
}
return null;
};
const activeMenu = findMenuByEngNm(menuList);
if (activeMenu?.urlPath) {
return activeMenu.urlPath;
}
}
// 탭에서 직접 찾기
if (activeTabId) {
const currentTabs = getTabStoreState().tabs;
const activeTab = currentTabs.find((t) => t.id === activeTabId);
if (activeTab?.menuItem?.urlPath) {
return activeTab.menuItem.urlPath;
}
}
// currentMenuInfo에서 가져오기
if (currentMenuInfo?.urlPath) {
return currentMenuInfo.urlPath;
}
// 기본값으로 pathname 사용
return pathname ?? "";
}, [tabs.length, activeTabId, menuList, currentMenuInfo, pathname]);
const handleMenuClick = useCallback(
(item: MenuItem) => {
if (item.urlPath.includes("/cms/")) return;
setCurrentMenuInfo(item);
if (isVerificationSelectionReportMenu(item)) {
setReportPanelOpen(true);
return;
}
if (isBbsNoticeMenu(item)) {
if (item.urlPath) {
router.push(item.urlPath);
}
return;
}
const addTabResult: boolean = addTab(item);
// packages/rsone의 스토어에도 동기화
if (addTabResult) {
addTabForGrid(item);
if (item.urlPath) {
syncTabToUrl(item.urlPath);
syncTabToUrl(resolveStatisticsTabId(item.urlPath));
}
}
if (!addTabResult) {
alert(`최대 추가 할 수 있는 화면 수(10개)를 \n초과 하였습니다.
기존 화면을 닫은 후 이용해주세요.`);
}
},
[
setCurrentMenuInfo,
addTab,
@@ -354,70 +236,38 @@ function WithBaseLayoutInner({
syncTabToUrl,
router,
],
);
return (
<BaseLayout
queryClient={queryClient}
tokenManager={tokenManager}
currentPath={currentPath}
handleMenuClick={handleMenuClick}
>
<div className="flex flex-1 h-full">
<div className="flex h-full flex-1">
{isVerificationSelectionReportPanelOpen && (
<VerificationSelectionReport
onClose={() => setReportPanelOpen(false)}
/>
)}
<StatisticsPageLayout>{children}</StatisticsPageLayout>
</div>
</BaseLayout>
);
}
export default function WithBaseLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<Suspense fallback={null}>
<WithBaseLayoutInner>{children}</WithBaseLayoutInner>
</Suspense>
);
}
@@ -0,0 +1,63 @@
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
import {
AUTH_USER_ID,
CLAIM_NAME,
SITE_ID,
ST_SERVICE_URL,
} from "@/shared/config";
import { SEWERAGE_AGGREGATE_SUMMARY_EXCEL_MIME } from "@/entities/statistics/model/statistics-total/statistics-sewerage-aggregate-summary-interface";
const BACKEND_DOWNLOAD_URL = `${ST_SERVICE_URL}/st-management-service/api/v1/summary-excel/download`;
/** middleware `/server` rewrite는 장시간 blob 응답에서 연결이 끊길 수 있어 전용 프록시 사용 */
export const dynamic = "force-dynamic";
export const maxDuration = 3600;
export async function POST(request: Request) {
const cookieStore = await cookies();
const token = cookieStore.get(CLAIM_NAME)?.value;
const userId = cookieStore.get(AUTH_USER_ID)?.value;
const headers: Record<string, string> = {
"Content-Type": "application/json",
"X-Site-Id": SITE_ID,
Accept: SEWERAGE_AGGREGATE_SUMMARY_EXCEL_MIME,
};
if (token) headers[CLAIM_NAME] = token;
if (userId) headers[AUTH_USER_ID] = userId;
let body: string;
try {
body = await request.text();
} catch {
return NextResponse.json({ message: "요청 본문을 읽을 수 없습니다." }, { status: 400 });
}
let backendRes: Response;
try {
backendRes = await fetch(BACKEND_DOWNLOAD_URL, {
method: "POST",
headers,
body,
cache: "no-store",
});
} catch {
return NextResponse.json(
{ message: "엑셀 서버에 연결할 수 없습니다." },
{ status: 502 },
);
}
const responseHeaders = new Headers();
const contentType = backendRes.headers.get("content-type");
const contentDisposition = backendRes.headers.get("content-disposition");
if (contentType) responseHeaders.set("Content-Type", contentType);
if (contentDisposition) responseHeaders.set("Content-Disposition", contentDisposition);
return new NextResponse(backendRes.body, {
status: backendRes.status,
headers: responseHeaders,
});
}
@@ -0,0 +1,24 @@
.verification-result-btn-wrap {
position: relative;
display: inline-flex;
}
.verification-result-btn-failed-dot {
position: absolute;
left: 0.35rem;
top: 0.35rem;
width: 0.55rem;
height: 0.55rem;
border-radius: 9999px;
background: #ff6c64;
box-shadow: 0 0 0 0 rgba(255, 108, 100, 0.75);
animation: verification-result-btn-dot-ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
pointer-events: none;
}
@keyframes verification-result-btn-dot-ping {
75%,
100% {
box-shadow: 0 0 0 0.5rem rgba(255, 108, 100, 0);
}
}
@@ -1,16 +1,34 @@
import { Button } from "@common/components";
import "./VerificationResultButton.css";
interface VerificationResultButtonProps {
VerificationResultButtonFunc: () => void;
isVerificationFailed?: boolean;
}
const VerificationResultButton = ({ VerificationResultButtonFunc }: VerificationResultButtonProps) => {
const VerificationResultButton = ({
VerificationResultButtonFunc,
isVerificationFailed = false,
}: VerificationResultButtonProps) => {
return (
<Button color="white" className="h-[3.2rem]" onClick={VerificationResultButtonFunc}>
<i className="diveicon di-list" />
<span>지난 검증 결과</span>
</Button>
<span className="verification-result-btn-wrap">
<Button
color="white"
className="h-[3.2rem]"
onClick={VerificationResultButtonFunc}
>
<i className="diveicon di-list" />
<span>지난 검증 결과</span>
</Button>
{isVerificationFailed && (
<span
className="verification-result-btn-failed-dot"
aria-hidden
/>
)}
</span>
);
};
export default VerificationResultButton;
export default VerificationResultButton;
+37 -1
View File
@@ -6,7 +6,10 @@
}
/* 셀 클래스가 있으면 위 기본색보다 우선 (같은 specificity + 나중 규칙) */
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly {
@apply !bg-[#f9f8f8] !text-[#666];
@apply !bg-[#f0f0f0] !text-[#666];
}
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly.rg-cell-rowtype-curr {
@apply !bg-white !text-[#333];
}
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error {
@apply !bg-[#ffe6e6] !text-[#c00];
@@ -153,6 +156,39 @@
.rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-rowtype-other {
@apply !bg-[#fff7bf];
}
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-rowtype-curr,
.rg-grid .rg-body .rg-data-cell.rg-cell-rowtype-curr {
@apply !bg-white !text-[#333];
}
.rg-grid .rg-fixed-body .rg-table .rg-data-row td.rg-cell-rowtype-curr,
.rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-rowtype-curr,
.rg-grid .rg-fixed-body .rg-data-cell.rg-cell-readonly.rg-cell-rowtype-curr {
@apply !bg-white !text-[#333];
}
/* 하수관로 검토: CURR 구분 이후 미입력 — OLD 있음(노랑) / OLD 없음(하늘) */
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly.rg-cell-review-curr-empty-warn,
.rg-grid .rg-body .rg-data-cell.rg-cell-readonly.rg-cell-review-curr-empty-warn,
.rg-grid .rg-fixed-body .rg-table .rg-data-row td.rg-cell-readonly.rg-cell-review-curr-empty-warn,
.rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-readonly.rg-cell-review-curr-empty-warn,
.rg-grid .rg-body .rg-data-cell.rg-cell-review-curr-empty-warn.rg-cell-review-curr-empty-warn,
.rg-grid .rg-fixed-body .rg-data-cell.rg-cell-review-curr-empty-warn.rg-cell-review-curr-empty-warn,
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-rowtype-curr.rg-cell-review-curr-empty-warn,
.rg-grid .rg-body .rg-data-cell.rg-cell-rowtype-curr.rg-cell-review-curr-empty-warn,
.rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-rowtype-curr.rg-cell-review-curr-empty-warn {
@apply !bg-[#feee85] !text-[#333];
}
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly.rg-cell-review-curr-empty-new,
.rg-grid .rg-body .rg-data-cell.rg-cell-readonly.rg-cell-review-curr-empty-new,
.rg-grid .rg-fixed-body .rg-table .rg-data-row td.rg-cell-readonly.rg-cell-review-curr-empty-new,
.rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-readonly.rg-cell-review-curr-empty-new,
.rg-grid .rg-body .rg-data-cell.rg-cell-review-curr-empty-new.rg-cell-review-curr-empty-new,
.rg-grid .rg-fixed-body .rg-data-cell.rg-cell-review-curr-empty-new.rg-cell-review-curr-empty-new,
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-rowtype-curr.rg-cell-review-curr-empty-new,
.rg-grid .rg-body .rg-data-cell.rg-cell-rowtype-curr.rg-cell-review-curr-empty-new,
.rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-rowtype-curr.rg-cell-review-curr-empty-new {
@apply !bg-[#bae6fd] !text-[#333];
}
/*
* 검증 오류 셀(rg-cell-verify-error / -imr) 최우선 적용.
@@ -3,9 +3,27 @@ import { ValueType } from "realgrid"
import RealGrid, { GridFitStyle } from "realgrid"
import { RealGridReact, RGDataColumn } from "realgrid-react"
import type { DataGridProps, RealGridColumnInfo, GridViewWithSetColumns, FooterItemWithCallback, CellStyleCallback } from "../type/DataGrid.type"
import type { DataGridProps, RealGridColumnInfo, GridViewWithSetColumns, FooterItemWithCallback, CellStyleCallback, CellStyleCallbackResult, CellStyleCallbackParams } from "../type/DataGrid.type"
import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"
function mergeCellStyleCallbackResult(
baseStyleName: string | undefined,
extra: CellStyleCallbackResult | undefined,
): CellStyleCallbackResult {
if (!extra) {
return baseStyleName ? { styleName: baseStyleName } : {};
}
if (typeof extra === "string") {
const styleName = [extra, baseStyleName].filter(Boolean).join(" ");
return styleName ? { styleName } : {};
}
const styleName = [extra.styleName, baseStyleName].filter(Boolean).join(" ");
return {
...extra,
...(styleName ? { styleName } : {}),
};
}
// RealGrid 타입 확장
declare module "realgrid" {
interface GridView {
@@ -40,11 +58,14 @@ function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueT
};
if (col.computed) {
const hasFields = "fields" in col.computed && col.computed.fields.length > 0;
const hasNumeratorDenominator = "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && col.computed.denominatorFields.length > 0;
const hasNumeratorDenominator = "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && "denominatorFields" in col.computed && col.computed.denominatorFields.length > 0;
const hasMaxOfFieldAndSum = "baseField" in col.computed && "sumFields" in col.computed && col.computed.sumFields.length > 0;
const hasDifferenceOfSums = "minuendFields" in col.computed && "subtrahendFields" in col.computed && col.computed.minuendFields.length > 0;
const hasHalfSumOfSums = col.computed.type === "halfSumOfSums" && "firstSumFields" in col.computed && "secondSumFields" in col.computed && col.computed.firstSumFields.length > 0 && col.computed.secondSumFields.length > 0;
const hasSumOfTwoHalfSumOfSums = col.computed.type === "sumOfTwoHalfSumOfSums" && "firstHalfSum" in col.computed && "secondHalfSum" in col.computed && col.computed.firstHalfSum.firstSumFields.length > 0 && col.computed.firstHalfSum.secondSumFields.length > 0 && col.computed.secondHalfSum.firstSumFields.length > 0 && col.computed.secondHalfSum.secondSumFields.length > 0;
const hasSumOfMaxGroups = "groups" in col.computed && Array.isArray(col.computed.groups) && col.computed.groups.length > 0;
if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasSumOfMaxGroups) {
const hasSumPerTenThousand = col.computed.type === "sumPerTenThousand" && "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && "denominatorField" in col.computed && Boolean(col.computed.denominatorField);
if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasHalfSumOfSums || hasSumOfTwoHalfSumOfSums || hasSumOfMaxGroups || hasSumPerTenThousand) {
field.valueCallback = getComputedValueCallback(col.computed) as (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number;
}
}
@@ -408,18 +429,33 @@ const CompactDataGrid = ({
columns.forEach((column) => {
const baseStyleName = column.styleName;
const gridCol = currentGridView.columnByName(column.name) as {
styleCallback?: (g: unknown, dataCell: { itemIndex?: number; index?: { itemIndex?: number }; dataField?: string; fieldName?: string }) => string;
styleCallback?: (
g: unknown,
dataCell: {
itemIndex?: number;
index?: { itemIndex?: number };
dataField?: string;
fieldName?: string;
},
) => CellStyleCallbackResult;
} | null;
if (gridCol) {
gridCol.styleCallback = (_g, dataCell) => {
gridCol.styleCallback = (grid, dataCell) => {
const itemIndex = dataCell?.index?.itemIndex ?? dataCell?.itemIndex ?? -1;
const fieldName = (dataCell?.dataField ?? dataCell?.fieldName ?? column.fieldName) ?? '';
const fieldName =
(dataCell?.dataField ?? dataCell?.fieldName ?? column.fieldName) ?? "";
const data = rowDataRef.current;
if (itemIndex < 0 || itemIndex >= data.length) return baseStyleName ?? '';
if (itemIndex < 0 || itemIndex >= data.length) {
return mergeCellStyleCallbackResult(baseStyleName, undefined);
}
const row = data[itemIndex] as Record<string, string | number>;
const extra = cellStyleCallbackRef.current?.({ rowIndex: itemIndex, fieldName, row });
if (!extra) return baseStyleName ?? '';
return baseStyleName ? `${extra} ${baseStyleName}` : extra;
const extra = cellStyleCallbackRef.current?.({
rowIndex: itemIndex,
fieldName,
row,
grid: grid as CellStyleCallbackParams["grid"],
});
return mergeCellStyleCallbackResult(baseStyleName, extra);
};
}
});
+400 -131
View File
@@ -1,4 +1,4 @@
import { useEffect, useLayoutEffect, useRef } from "react";
import { useEffect, useLayoutEffect, useRef, type MutableRefObject } from "react";
import JSZip from "jszip";
import { ValueType } from "realgrid";
@@ -11,7 +11,10 @@ import { Button } from "@common/components";
import type {
CellStyleCallback,
CellStyleCallbackParams,
CellStyleCallbackResult,
CellTooltipCallback,
ColumnConfig,
ColumnLayoutItem,
DataGridProps,
FooterItemWithCallback,
@@ -21,19 +24,114 @@ import type {
import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks";
import {
getExcelImportColumnFieldNames,
prepareGridColumnsForExcelExport,
restoreGridColumnsAfterExcelExport,
shouldPrepareColumnsForExcelExport,
} from "@/components/util/gridExcelExport";
import { useTabStore } from "@/store/tab-store";
function mergeCellStyleCallbackResult(
baseStyleName: string | undefined,
extra: CellStyleCallbackResult | undefined,
): CellStyleCallbackResult {
if (!extra) {
return baseStyleName ? { styleName: baseStyleName } : {};
}
if (typeof extra === "string") {
const styleName = [baseStyleName, extra].filter(Boolean).join(" ");
return styleName ? { styleName } : {};
}
const styleName = [baseStyleName, extra.styleName].filter(Boolean).join(" ");
return {
...extra,
...(styleName ? { styleName } : {}),
};
}
type GridColumnWithStyleCallback = {
styleCallback?: (
grid: unknown,
dataCell: {
itemIndex?: number;
index?: { itemIndex?: number };
dataField?: string;
fieldName?: string;
},
) => CellStyleCallbackResult;
};
function mergeGridRowsWithSourceMetadata(
rows: Record<string, string | number>[],
sourceRows: Record<string, string | number>[],
columnFieldNames: string[],
): Record<string, string | number>[] {
const columnFieldSet = new Set(columnFieldNames);
return rows.map((row, index) => {
const source = sourceRows[index];
if (!source) return row;
const merged: Record<string, string | number> = { ...row };
for (const [key, value] of Object.entries(source)) {
if (columnFieldSet.has(key) || value === undefined) continue;
merged[key] = value as string | number;
}
return merged;
});
}
function applyCellStyleCallbacksToColumns(
gridView: unknown,
columns: ColumnConfig[],
rowDataRef: MutableRefObject<Record<string, string | number>[]>,
cellStyleCallbackRef: MutableRefObject<CellStyleCallback | undefined>,
) {
if (!cellStyleCallbackRef.current || !gridView) return;
const gv = gridView as {
columnByName?: (name: string) => GridColumnWithStyleCallback | null | undefined;
};
if (typeof gv.columnByName !== "function") return;
columns.forEach((column) => {
const baseStyleName = column.styleName;
let gridCol: GridColumnWithStyleCallback | null | undefined;
try {
// columnByName은 GridView 인스턴스 메서드 — this 바인딩을 위해 gv에서 직접 호출
gridCol = gv.columnByName!(column.name);
} catch {
return;
}
if (!gridCol) return;
gridCol.styleCallback = (grid, dataCell) => {
const itemIndex =
dataCell?.index?.itemIndex ?? dataCell?.itemIndex ?? -1;
const fieldName =
dataCell?.dataField ?? dataCell?.fieldName ?? column.fieldName;
const data = rowDataRef.current;
if (itemIndex < 0 || itemIndex >= data.length) {
return mergeCellStyleCallbackResult(baseStyleName, undefined);
}
const row = data[itemIndex] as Record<string, string | number>;
const extra = cellStyleCallbackRef.current?.({
rowIndex: itemIndex,
fieldName,
row,
grid: grid as CellStyleCallbackParams["grid"],
});
return mergeCellStyleCallbackResult(baseStyleName, extra);
};
});
}
// RealGrid 타입 확장
declare module "realgrid" {
interface GridView {
setOptions(options: {
footer?: { visible?: boolean };
summaryMode?: "aggregate" | "statistical" | "none";
checkBar?: { visible?: boolean };
checkBar?: { visible?: boolean; exclusive?: boolean };
}): void;
}
}
@@ -457,10 +555,31 @@ function sortRowsGroupPreserving(
}
/** columnInfo → RealGrid DataField 배열 (computed → valueCallback 포함, setFields용) */
function getDecimalPlacesFromDataFormat(dataFormat?: string): number | null {
if (!dataFormat) return null;
const match = dataFormat.match(/\.(0+|#+)/);
const decimalToken = match?.[1];
if (decimalToken) return decimalToken.length;
// #,##0 등 정수 포맷 — 소수부 없음 → 0자리 절삭(내림)
if (/[#0]/.test(dataFormat)) return 0;
return null;
}
function truncateByDecimalPlaces(value: number, decimalPlaces: number): number {
if (!Number.isFinite(value)) return 0;
if (decimalPlaces < 0) return value;
const factor = 10 ** decimalPlaces;
const scaled = value * factor;
// 절삭(truncate): 반올림 없이 소수점 이하를 버린다.
const truncated = scaled >= 0 ? Math.floor(scaled) : Math.ceil(scaled);
return truncated / factor;
}
function buildFieldsFromColumns(
columns: {
fieldName: string;
valueType?: ValueType;
dataFormat?: string;
datetimeFormat?: string;
editor?: { type?: string; datetimeFormat?: string };
computed?: import("../type/DataGrid.type").ComputedFieldConfig;
@@ -499,6 +618,7 @@ function buildFieldsFromColumns(
const hasNumeratorDenominator =
"numeratorFields" in col.computed &&
col.computed.numeratorFields.length > 0 &&
"denominatorFields" in col.computed &&
col.computed.denominatorFields.length > 0;
const hasMaxOfFieldAndSum =
"baseField" in col.computed &&
@@ -508,18 +628,68 @@ function buildFieldsFromColumns(
"minuendFields" in col.computed &&
"subtrahendFields" in col.computed &&
col.computed.minuendFields.length > 0;
const hasHalfSumOfSums =
col.computed.type === "halfSumOfSums" &&
"firstSumFields" in col.computed &&
"secondSumFields" in col.computed &&
col.computed.firstSumFields.length > 0 &&
col.computed.secondSumFields.length > 0;
const hasSumOfTwoHalfSumOfSums =
col.computed.type === "sumOfTwoHalfSumOfSums" &&
"firstHalfSum" in col.computed &&
"secondHalfSum" in col.computed &&
col.computed.firstHalfSum.firstSumFields.length > 0 &&
col.computed.firstHalfSum.secondSumFields.length > 0 &&
col.computed.secondHalfSum.firstSumFields.length > 0 &&
col.computed.secondHalfSum.secondSumFields.length > 0;
const hasSumOfMaxGroups =
col.computed.type === "sumOfMaxOfFieldAndSumGroups" &&
"groups" in col.computed &&
Array.isArray(col.computed.groups) &&
col.computed.groups.length > 0;
const hasPumpGrossPpqty =
col.computed.type === "pumpGrossPpqty" &&
"pumpGroups" in col.computed &&
col.computed.pumpGroups.length > 0;
const hasBodPrcsRt =
col.computed.type === "bodPrcsRt" &&
"inflwField" in col.computed &&
"dschgField" in col.computed;
const hasSumPerTenThousand =
col.computed.type === "sumPerTenThousand" &&
"numeratorFields" in col.computed &&
col.computed.numeratorFields.length > 0 &&
"denominatorField" in col.computed &&
Boolean(col.computed.denominatorField);
const hasQuotientRatioPercent =
col.computed.type === "quotientRatioPercent" &&
"numeratorQuotient" in col.computed &&
"denominatorQuotient" in col.computed &&
col.computed.numeratorQuotient.length === 2 &&
col.computed.denominatorQuotient.length === 2;
const hasQuotientTimesMultiplierPercent =
col.computed.type === "quotientTimesMultiplierPercent" &&
"numeratorField" in col.computed &&
"denominatorField" in col.computed &&
"multiplierField" in col.computed &&
Boolean(col.computed.numeratorField) &&
Boolean(col.computed.denominatorField) &&
Boolean(col.computed.multiplierField);
if (
hasFields ||
hasNumeratorDenominator ||
hasMaxOfFieldAndSum ||
hasDifferenceOfSums ||
hasSumOfMaxGroups
hasHalfSumOfSums ||
hasSumOfTwoHalfSumOfSums ||
hasSumOfMaxGroups ||
hasPumpGrossPpqty ||
hasBodPrcsRt ||
hasSumPerTenThousand ||
hasQuotientRatioPercent ||
hasQuotientTimesMultiplierPercent
) {
field.valueCallback = getComputedValueCallback(
const computedCallback = getComputedValueCallback(
col.computed,
) as (
prod: unknown,
@@ -528,6 +698,24 @@ function buildFieldsFromColumns(
fieldNames: string[],
values: unknown[],
) => number;
const decimalPlaces = getDecimalPlacesFromDataFormat(col.dataFormat);
field.valueCallback = (
prod: unknown,
dataRow: number,
fieldName: string,
fieldNames: string[],
values: unknown[],
) => {
const computedValue = computedCallback(
prod,
dataRow,
fieldName,
fieldNames,
values,
);
if (decimalPlaces == null) return computedValue;
return truncateByDecimalPlaces(computedValue, decimalPlaces);
};
}
}
return field;
@@ -551,6 +739,7 @@ const DataGrid = ({
onCellEdited,
onRowClick,
onDataCellClick,
onCellDblClick,
onItemChecked,
onItemAllChecked,
onScrollToBottom,
@@ -586,6 +775,7 @@ const DataGrid = ({
const onItemAllCheckedRef =
useRef<typeof onItemAllChecked>(onItemAllChecked);
const isSyncingChecksFromRowDataRef = useRef(false);
const columnsFieldsSigRef = useRef("");
columnsRef.current = columns;
rowDataRef.current = rowData;
cellStyleCallbackRef.current = cellStyleCallback;
@@ -596,6 +786,7 @@ const DataGrid = ({
onCellEditedRef.current = onCellEdited;
onDataCellClickRef.current = onDataCellClick;
// highlightedRowId가 변경될 때 해당 행으로 스크롤 및 선택
useEffect(() => {
if (!gridRef.current?.gridView || highlightedRowId == null) return;
@@ -611,11 +802,54 @@ const DataGrid = ({
gridView.setFocus();
}
}, [highlightedRowId, rowData, idFieldName]);
// 행 더블클릭 이벤트
useEffect(() => {
const gridView = gridRef.current?.gridView;
if (!gridView || !onCellDblClick) return;
gridView.onCellDblClicked = (_grid, clickData) => {
if (clickData.cellType !== "data" || clickData.itemIndex === undefined) return;
const row = rowDataRef.current[clickData.itemIndex];
const cellIndex = gridView.getCurrent();
const column = cellIndex.column;
const fieldName =
cellIndex.fieldName ??
(typeof column === "string" ? column : "");
if (row) {
onCellDblClick({
row: row as Record<string, string | number>,
fieldName,
itemIndex: clickData.itemIndex,
});
}
};
return () => {
gridView.onCellDblClicked = undefined;
};
}, [onCellDblClick]);
useLayoutEffect(() => {
if (!cellStyleCallback || !gridRef.current?.gridView) return;
gridRef.current.gridView.refresh();
}, [cellStyleCallback, rowData]);
const gv = gridRef.current?.gridView;
if (!gv || !cellStyleCallback) return;
const apply = () => {
applyCellStyleCallbacksToColumns(
gv,
columns,
rowDataRef,
cellStyleCallbackRef,
);
(gv as { refresh?: () => void }).refresh?.();
};
apply();
// footer setColumns / setColumnLayout 비동기 처리 이후 styleCallback 재적용
const timerId = window.setTimeout(apply, 300);
return () => {
window.clearTimeout(timerId);
};
}, [cellStyleCallback, columns, rowData]);
const { setMessage: alert } = useAlertStore((state) => state);
const activeTabId = useTabStore((state) => state.activeTabId);
@@ -638,20 +872,6 @@ const DataGrid = ({
.find((s) => s.menuEngNm === activeTabId)?.menuKornNm ||
"export";
// 엑셀 import 시 매칭을 위해 헤더에 fieldName 포함 (예: "합계 (swgeProcZoneInnerSm)")
const originalHeaders: Map<string, string> = new Map();
columns.forEach((col) => {
if (col.fieldName && gridView.columnByName) {
const gridCol = gridView.columnByName(col.name);
if (gridCol && gridCol.header) {
const originalText = gridCol.header.text || "";
originalHeaders.set(col.name, originalText);
const headerWithFieldName = `${originalText} (${col.fieldName})`;
gridCol.header.text = headerWithFieldName;
}
}
});
// exportGrid 호출 전에 columnLayout 정규화 (col 속성 추가)
const normalizeLayoutForExport = (
layout: ColumnLayoutItem[],
@@ -745,20 +965,6 @@ const DataGrid = ({
excelExportVisibilitySnapshot,
);
}
// 헤더 원복
columns.forEach((col) => {
if (col.fieldName && gridView.columnByName) {
const gridCol = gridView.columnByName(col.name);
if (
gridCol &&
gridCol.header &&
originalHeaders.has(col.name)
) {
gridCol.header.text =
originalHeaders.get(col.name) || "";
}
}
});
// columnLayout 원복 (footerUserSpans 유지)
if (normalizedLayout && !layoutRestored) {
layoutRestored = true;
@@ -860,36 +1066,11 @@ const DataGrid = ({
: headerStartRow;
const dataStartRow = maxHeaderRowIndex + 1;
// 엑셀 컬럼 인덱스 → 그리드 fieldName 매핑
const headerToFieldNameMap = new Map<string, string>();
headers.forEach((headerText, colIndex) => {
let fieldName: string | null = null;
// 헤더에 fieldName이 포함된 경우 (예: "합계 (swgeProcZoneInnerSm)")
const fieldNameMatch = headerText.match(
/\(([a-zA-Z_][a-zA-Z0-9_]*)\)$/,
);
if (fieldNameMatch && fieldNameMatch[1]) {
const extractedFieldName = fieldNameMatch[1];
if (
columns.find(
(c) => c.fieldName === extractedFieldName,
)
) {
fieldName = extractedFieldName;
}
}
// 매칭 실패 시 원본 헤더 텍스트 사용
if (!fieldName) {
console.warn(
`엑셀 컬럼 ${colIndex + 1} (헤더: "${headerText}") 매칭 실패 → 헤더 텍스트를 fieldName으로 사용`,
);
fieldName = headerText;
}
headerToFieldNameMap.set(headerText, fieldName);
});
// 엑셀 N번째 열 → 그리드 N번째 fieldName (보내기 컬럼 순서 기준)
const excelColumnFieldNames = getExcelImportColumnFieldNames(
columns,
columnLayout,
);
// 데이터 행 읽기
const dataRows: Record<string, string | number>[] = [];
@@ -907,12 +1088,8 @@ const DataGrid = ({
worksheet[
XLSX.utils.encode_cell({ r: row, c: col })
];
const headerName =
headers[colIndex] || `Column${colIndex + 1}`;
// 헤더 텍스트를 fieldName으로 변환
const fieldName =
headerToFieldNameMap.get(headerName) ||
headerName;
const fieldName = excelColumnFieldNames[colIndex];
if (!fieldName) continue;
let cellValue: string | number = "";
if (
@@ -994,8 +1171,27 @@ const DataGrid = ({
const gridView = gridRef.current.gridView;
const dataProvider = gridRef.current.dataProvider;
const footerTimerIds: ReturnType<typeof setTimeout>[] = [];
const scheduleFooterTask = (task: () => void, delayMs: number) => {
footerTimerIds.push(setTimeout(task, delayMs));
};
applyDefaultGridEditOptions(gridView as GridViewWithEditOptions);
const columnsFieldsSig = columns.map((c) => c.fieldName).join("\u0001");
if (columnsFieldsSig !== columnsFieldsSigRef.current) {
columnsFieldsSigRef.current = columnsFieldsSig;
const dpForFields = dataProvider as {
setFields?: (f: unknown[]) => void;
setRows?: (r: Record<string, string | number>[]) => void;
};
try {
dpForFields.setFields?.(buildFieldsFromColumns(columns));
dpForFields.setRows?.(rowDataRef.current);
} catch (e) {
console.warn("setFields sync:", e);
}
}
// mergeCells: true인 컬럼이 있으면 onSorting에서 클라이언트 정렬로 처리
const hasMergeCells = columns.some((col) => col.mergeCells === true);
@@ -1046,6 +1242,14 @@ const DataGrid = ({
}
columns.forEach((column) => {
const gv = gridView as GridViewWithSetColumnProperty;
const exists =
typeof (
gridView as { columnByName?: (name: string) => unknown }
).columnByName === "function" &&
(gridView as { columnByName: (name: string) => unknown }).columnByName(
column.name,
) != null;
if (!exists) return;
gv.setColumnProperty?.(
column.name,
"header",
@@ -1054,6 +1258,11 @@ const DataGrid = ({
tooltip: resolveColumnTooltip(column),
}),
);
gv.setColumnProperty?.(
column.name,
"editable",
column.editable !== false,
);
applyColumnListEditorProperties(gv, column);
});
if (columns.some((c) => isDropDownEditorColumn(c))) {
@@ -1101,13 +1310,15 @@ const DataGrid = ({
// 헤더 체크박스 표시 여부 설정
gridView.setOptions({
checkBar: { visible: showHeaderCheckbox },
checkBar: { visible: showHeaderCheckbox, exclusive: false },
rowIndicator: { visible: false },
});
const applyFooterSettings = (
targetGridView: typeof gridView,
targetGridView: typeof gridView | null | undefined,
) => {
if (!targetGridView?.setOptions) return;
const hasFooter = columns.some((col) => col.footer);
if (!hasFooter) return;
@@ -1181,20 +1392,79 @@ const DataGrid = ({
const hasFooterUserSpans = checkFooterUserSpans(columnLayout);
if (hasFooterUserSpans) {
const validColumnNames = new Set(columns.map((c) => c.name));
const sanitizeColumnLayout = (
layout: unknown[],
): unknown[] => {
const result: unknown[] = [];
for (const item of layout) {
if (typeof item === "string") {
if (validColumnNames.has(item)) {
result.push(item);
}
continue;
}
if (!item || typeof item !== "object") continue;
if ("column" in item) {
const columnName = (item as { column?: unknown }).column;
if (
typeof columnName === "string" &&
validColumnNames.has(columnName)
) {
result.push(item);
}
continue;
}
if ("items" in item && Array.isArray((item as { items?: unknown[] }).items)) {
const nextItems = sanitizeColumnLayout(
(item as { items: unknown[] }).items,
);
if (nextItems.length > 0) {
result.push({
...(item as Record<string, unknown>),
items: nextItems,
});
}
continue;
}
result.push(item);
}
return result;
};
// setColumnLayout은 그리드가 완전히 렌더링된 후 호출해야 함
setTimeout(() => {
scheduleFooterTask(() => {
if (gridRef.current?.gridView && columnLayout) {
gridRef.current.gridView.setColumnLayout?.(columnLayout);
const safeLayout = sanitizeColumnLayout(columnLayout as unknown[]);
if (safeLayout.length === 0) return;
try {
(
gridRef.current.gridView.setColumnLayout as
| ((layout: unknown) => void)
| undefined
)?.(safeLayout);
applyCellStyleCallbacksToColumns(
gridRef.current.gridView,
columns,
rowDataRef,
cellStyleCallbackRef,
);
gridRef.current.gridView.refresh?.();
} catch (e) {
console.warn("setColumnLayout skipped due to invalid columns:", e);
}
}
}, 100);
}
// footer 설정 (배열 또는 단일 객체 모두 처리)
// setFooters 호출 후 footer 배열 설정
setTimeout(() => {
scheduleFooterTask(() => {
const currentGridView = gridRef.current?.gridView;
if (!currentGridView || !currentGridView.columnByName) {
console.warn("GridView not available for footer setup");
if (!currentGridView?.columnByName) {
return;
}
@@ -1253,15 +1523,24 @@ const DataGrid = ({
if (hasFooterUserSpans) {
currentGridView.setColumnLayout?.(columnLayout);
}
applyCellStyleCallbacksToColumns(
currentGridView,
columns,
rowDataRef,
cellStyleCallbackRef,
);
// setColumns 호출 후 footer 배열의 valueCallback을 다시 설정
setTimeout(() => {
scheduleFooterTask(() => {
const liveGridView = gridRef.current?.gridView;
if (!liveGridView?.columnByName) return;
columns.forEach((col) => {
if (!col.footer || !Array.isArray(col.footer))
return;
try {
const column = currentGridView.columnByName(
const column = liveGridView.columnByName(
col.name,
);
if (!column) return;
@@ -1293,11 +1572,17 @@ const DataGrid = ({
);
}
});
applyFooterSettings(currentGridView);
applyFooterSettings(liveGridView);
if (hasFooterUserSpans) {
currentGridView.setColumnLayout?.(columnLayout);
liveGridView.setColumnLayout?.(columnLayout);
}
currentGridView.refresh?.();
applyCellStyleCallbacksToColumns(
liveGridView,
columns,
rowDataRef,
cellStyleCallbackRef,
);
liveGridView.refresh?.();
}, 50);
} catch (e) {
console.warn("Failed to set columns with footer array:", e);
@@ -1354,11 +1639,16 @@ const DataGrid = ({
r[f] = ds.getValue(i, f) as string | number;
rows.push(r);
}
return rows;
return mergeGridRowsWithSourceMetadata(
rows,
rowDataRef.current,
fieldNames,
);
};
}
return () => {
footerTimerIds.forEach(clearTimeout);
if (getGridDataRef) getGridDataRef.current = null;
};
}, [
@@ -1654,6 +1944,22 @@ const DataGrid = ({
layout={columnLayout}
rows={rowData}
onRGDataColumnsChanged={(grid) => {
const dp = gridRef.current?.dataProvider as
| {
setFields?: (f: unknown[]) => void;
setRows?: (
r: Record<string, string | number>[],
) => void;
}
| undefined;
try {
dp?.setFields?.(
buildFieldsFromColumns(columnsRef.current),
);
dp?.setRows?.(rowDataRef.current);
} catch (e) {
console.warn("onRGDataColumnsChanged setFields:", e);
}
reapplyListEditorsOnGrid(
grid as GridViewWithSetColumnProperty,
columnsRef.current,
@@ -1693,6 +1999,9 @@ const DataGrid = ({
dataProvider.setFields(
buildFieldsFromColumns(columns),
);
columnsFieldsSigRef.current = columns
.map((c) => c.fieldName)
.join("\u0001");
}
if (dataProvider?.setRows)
dataProvider.setRows(rowData);
@@ -1784,19 +2093,9 @@ const DataGrid = ({
};
}
columns.forEach((column) => {
const baseStyleName = column.styleName;
const gridCol = gv.columnByName(
column.name,
) as {
styleCallback?: (
g: unknown,
dataCell: {
itemIndex?: number;
index?: { itemIndex?: number };
dataField?: string;
fieldName?: string;
},
) => string;
renderer?: { showTooltip?: boolean };
} | null;
if (gridCol) {
@@ -1835,38 +2134,14 @@ const DataGrid = ({
{ showTooltip: true },
);
}
gridCol.styleCallback = (_g, dataCell) => {
const itemIndex =
dataCell?.index?.itemIndex ??
dataCell?.itemIndex ??
-1;
const fieldName =
dataCell?.dataField ??
dataCell?.fieldName ??
column.fieldName;
const data = rowDataRef.current;
if (
itemIndex < 0 ||
itemIndex >= data.length
)
return baseStyleName ?? "";
const row = data[itemIndex] as Record<
string,
string | number
>;
const extra =
cellStyleCallbackRef.current?.({
rowIndex: itemIndex,
fieldName,
row,
});
if (!extra) return baseStyleName ?? "";
return baseStyleName
? `${extra} ${baseStyleName}`
: extra;
};
}
});
applyCellStyleCallbacksToColumns(
gv,
columns,
rowDataRef,
cellStyleCallbackRef,
);
gv.refresh();
}, 0);
}}
@@ -1874,15 +2149,9 @@ const DataGrid = ({
{columns.map((column) => {
const baseStyleName = column.styleName;
const columnReactKey =
column.values?.length &&
isDropDownEditorColumn(column)
? `${column.name}-dropdown-${column.values.length}`
: column.name;
return (
<RGDataColumn
key={columnReactKey}
key={column.name}
name={column.name}
fieldName={column.fieldName}
valueType={
@@ -33,7 +33,7 @@ import DataGrid from "@/components/module/DataGrid";
import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination";
import type { CellStyleCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type";
/** 통계 입력 현황 > 행 클릭 시 예외처리(prcsYn=3) 내역만 조회 */
/** 통계 입력 현황 > 행 클릭 시 사용자예외처리(prcsYn=2) 내역만 조회 */
export interface ExceptionProcessingHistoryQueryContext {
sewerBzmnCd: string;
ctpvNm: string;
@@ -92,13 +92,15 @@ function exceptionPrcsYnNmCellStyle({ fieldName, row }: CellStyleCallbackParams)
const exceptionHistoryColumnInfo: ColumnInfo = {
columns: [
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역" }, editable: false },
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
{ name: "compVlCn", fieldName: "compVlCn", header: { text: "기준값" }, editable: false },
{ name: "prcsYnNm", fieldName: "prcsYnNm", header: { text: "처리여부" }, editable: false },
{ name: "pbcpAprvYn", fieldName: "pbcpAprvYn", header: { text: "공단승인여부" }, editable: false },
{ name: "stVrfRjtComNm", fieldName: "stVrfRjtComNm", header: { text: "반려상태" }, editable: false },
{ name: "prcsOpnnExpln", fieldName: "prcsOpnnExpln", header: { text: "처리의견" }, editable: false },
{ name: "rjctOpnnCn", fieldName: "rjctOpnnCn", header: { text: "반려의견" }, editable: false },
{ name: "regDt", fieldName: "regDt", header: { text: "등록일시" }, editable: false },
{ name: "mdfcnDt", fieldName: "mdfcnDt", header: { text: "갱신일시" }, editable: false },
{
@@ -114,13 +116,15 @@ const exceptionHistoryColumnInfo: ColumnInfo = {
],
columnLayout: [
"ctpvNm",
"sggNm",
"admdstNm",
"vrfcTrgtNm",
"rsnExpln",
"nowVlCn",
"compVlCn",
"prcsYnNm",
"pbcpAprvYn",
"stVrfRjtComNm",
"prcsOpnnExpln",
"rjctOpnnCn",
"regDt",
"mdfcnDt",
"individualReject",
@@ -190,9 +194,9 @@ export function ExceptionProcessingHistoryDialog({
sggNm: queryContext.sggNm,
statsYr: queryContext.statsYr,
statsCd: queryContext.statsCd,
pbcpAprvYn: "N",
vrfcMthdCd,
prcsYnList: PRCS_YN_LIST,
rejectCodeList: "0,1,2",
page: pageIndex + 1,
size: 30,
};
@@ -308,14 +312,16 @@ export function ExceptionProcessingHistoryDialog({
const mapRows = (entries: VerificationResultReportItem[]): ExceptionHistoryGridRow[] =>
entries.map((item) => ({
ctpvNm: item.ctpvNm,
sggNm: item.sggNm,
admdstNm: item.admdstNm ?? item.sggNm ?? "-",
vrfcTrgtNm: item.vrfcTrgtNm,
rsnExpln: item.rsnExpln,
nowVlCn: item.nowVlCn ?? "-",
compVlCn: item.compVlCn ?? "-",
prcsYnNm: item.prcsYnNm ?? "-",
stVrfRjtComNm: item.stVrfRjtComNm ?? "-",
prcsOpnnExpln: item.prcsOpnnExpln ?? "-",
rjctOpnnCn: item.rjctOpnnCn ?? "-",
prcsYn: item.prcsYn ?? "",
pbcpAprvYn: item.pbcpAprvYn,
regDt: (item.regDt ?? "").slice(0, 16),
mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16),
individualReject: "개별 반려",
+43 -17
View File
@@ -41,7 +41,7 @@ const getSggListFetcher = async (
key: [string, string, string, string],
): Promise<ApiResponse<Sgg[]>> => {
const gugunSkipYn = doList.includes(key[3]) ? "N" : "Y";
return statisticsApi.getSggAllList(key[1], {
return statisticsApi.getSggList(key[1], {
instSeCd: key[2],
gugunSkipYn,
});
@@ -137,11 +137,11 @@ const SearchForm = (props: SearchFormProps = {}) => {
prev.sido
? prev
: {
...prev,
sido: first.sido,
gugun: "",
bsnmCd: first.bsnmCd,
},
...prev,
sido: first.sido,
gugun: "",
bsnmCd: first.bsnmCd,
},
);
}, [sidoList, props.defaultValues?.sido, mergedProps.sido]);
@@ -172,6 +172,30 @@ const SearchForm = (props: SearchFormProps = {}) => {
);
const sggOptions = sggList?.data ?? [];
// 시도 설정 후 시군구 목록이 오면 첫 번째 항목 자동 선택
useEffect(() => {
if (!mergedProps.gugun) return;
if (props.defaultValues?.gugun) return;
if (!values.sido || values.sido === "ALL") return;
const firstSgg = sggList?.data?.[0];
if (!firstSgg) return;
setValues((prev) => {
if (prev.gugun) return prev;
return {
...prev,
gugun: firstSgg.gugun,
bsnmCd: firstSgg.bsnmCd,
};
});
}, [
sggList,
values.sido,
mergedProps.gugun,
props.defaultValues?.gugun,
]);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
props.onSubmit?.(values);
@@ -184,7 +208,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
const handleSidoChange = (value: string) => {
const selectedBsnm =
sidoList?.data.sidoList.find((v) => v.sido === value)?.bsnmCd || "";
sidoList?.data?.sidoList?.find((v) => v.sido === value)?.bsnmCd || "";
setValues((prev) => ({
...prev,
sido: value,
@@ -197,7 +221,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
const listForBsnm = sggList?.data ?? [];
const selectedBsnm =
value === "ALL"
? sidoList?.data.sidoList.find((v) => v.sido === values.sido)
? sidoList?.data?.sidoList?.find((v) => v.sido === values.sido)
?.bsnmCd || ""
: listForBsnm.find((v) => v.gugun === value)?.bsnmCd || "";
@@ -398,16 +422,18 @@ const SearchForm = (props: SearchFormProps = {}) => {
</div>
)}
<div className="ml-auto flex items-center gap-2">
<Button
color="white"
className="h-[3.2rem] w-[96px]"
type="button"
onClick={handleReset}
>
초기화
</Button>
{mergedProps.isReset && (
<Button
color="white"
className="h-[3.2rem] w-[96px]"
type="button"
onClick={handleReset}
>
초기화
</Button>
)}
<Button className="h-[3.2rem] w-[96px]" type="submit">
조회
{props.searchButtonText ? props.searchButtonText : '조회'}
</Button>
</div>
</div>
@@ -116,7 +116,7 @@ const getSidoListFetcher = async (): Promise<ApiResponse<SearchCondition>> => {
const getSggAllListFetcher = async (
key: [string, string],
): Promise<ApiResponse<Sgg[]>> => {
return statisticsApi.getSggAllList(key[1]);
return statisticsApi.getSggList(key[1]);
};
type SidoSggFacilityNameSearchFormProps = {
@@ -292,17 +292,12 @@ const SidoSggFacilityNameSearchForm = ({
return;
}
const ctpvNm = String(row.ctpvNm ?? sido);
const sggNm = String(
row.sggNm ?? (gugun !== "ALL" && gugun ? gugun : ""),
);
const params: SeweragePipeFacilityAdminAreaListRequest = {
statsYr: statsYr.trim(),
swtrmtFcltCd,
sewerBzmnCd: initialSearchValues?.bsnmCd || '',
ctpvNm,
...(doNameList.includes(ctpvNm) ? { sggNm } : {}),
ctpvNm: initialSidoValue || '',
...(doNameList.includes(initialSidoValue) ? { sggNm: initialGugunValue } : {}),
};
try {
+40 -57
View File
@@ -10,13 +10,32 @@ import { useAlertStore } from "@/shared/lib";
import { Tabs, TabsContent,TabsList, TabsTrigger } from "@common/components";
import { Button } from "@common/components";
import type { CellStyleCallback,ColumnConfig, ColumnInfo, ColumnLayoutItem, FooterItemWithCallback, GridViewWithSetColumns, RealGridColumnInfo, TabDataGridProps } from "../type/DataGrid.type"
import type { CellStyleCallback, CellStyleCallbackParams, CellStyleCallbackResult, ColumnConfig, ColumnInfo, ColumnLayoutItem, FooterItemWithCallback, GridViewWithSetColumns, RealGridColumnInfo, TabDataGridProps } from "../type/DataGrid.type"
import GridTopActionButton from "./GridTopActionButton";
function mergeCellStyleCallbackResult(
baseStyleName: string | undefined,
extra: CellStyleCallbackResult | undefined,
): CellStyleCallbackResult {
if (!extra) {
return baseStyleName ? { styleName: baseStyleName } : {};
}
if (typeof extra === "string") {
const styleName = [extra, baseStyleName].filter(Boolean).join(" ");
return styleName ? { styleName } : {};
}
const styleName = [extra.styleName, baseStyleName].filter(Boolean).join(" ");
return {
...extra,
...(styleName ? { styleName } : {}),
};
}
import SplitDataGrid from "./SplitDataGrid";
import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"
import {
getExcelImportColumnFieldNames,
prepareGridColumnsForExcelExport,
restoreGridColumnsAfterExcelExport,
shouldPrepareColumnsForExcelExport,
@@ -72,10 +91,12 @@ function safeGridViewRefresh(gv: { refresh?: () => void } | null | undefined) {
function safeColumnByName(
gv: { columnByName?: (name: string) => unknown } | null | undefined,
name: string,
): { styleCallback?: (g: unknown, dataCell: Record<string, unknown>) => string } | null {
): { styleCallback?: (g: unknown, dataCell: Record<string, unknown>) => CellStyleCallbackResult } | null {
if (!gv?.columnByName) return null;
try {
const col = gv.columnByName(name) as { styleCallback?: (g: unknown, dataCell: Record<string, unknown>) => string } | null;
const col = gv.columnByName(name) as {
styleCallback?: (g: unknown, dataCell: Record<string, unknown>) => CellStyleCallbackResult;
} | null;
return col ?? null;
} catch {
return null;
@@ -219,11 +240,14 @@ function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueT
};
if (col.computed) {
const hasFields = "fields" in col.computed && col.computed.fields.length > 0;
const hasNumeratorDenominator = "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && col.computed.denominatorFields.length > 0;
const hasNumeratorDenominator = "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && "denominatorFields" in col.computed && col.computed.denominatorFields.length > 0;
const hasMaxOfFieldAndSum = "baseField" in col.computed && "sumFields" in col.computed && col.computed.sumFields.length > 0;
const hasDifferenceOfSums = "minuendFields" in col.computed && "subtrahendFields" in col.computed && col.computed.minuendFields.length > 0;
const hasHalfSumOfSums = col.computed.type === "halfSumOfSums" && "firstSumFields" in col.computed && "secondSumFields" in col.computed && col.computed.firstSumFields.length > 0 && col.computed.secondSumFields.length > 0;
const hasSumOfTwoHalfSumOfSums = col.computed.type === "sumOfTwoHalfSumOfSums" && "firstHalfSum" in col.computed && "secondHalfSum" in col.computed && col.computed.firstHalfSum.firstSumFields.length > 0 && col.computed.firstHalfSum.secondSumFields.length > 0 && col.computed.secondHalfSum.firstSumFields.length > 0 && col.computed.secondHalfSum.secondSumFields.length > 0;
const hasSumOfMaxGroups = "groups" in col.computed && Array.isArray(col.computed.groups) && col.computed.groups.length > 0;
if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasSumOfMaxGroups) {
const hasSumPerTenThousand = col.computed.type === "sumPerTenThousand" && "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && "denominatorField" in col.computed && Boolean(col.computed.denominatorField);
if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasHalfSumOfSums || hasSumOfTwoHalfSumOfSums || hasSumOfMaxGroups || hasSumPerTenThousand) {
field.valueCallback = getComputedValueCallback(col.computed) as (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number;
}
}
@@ -292,7 +316,7 @@ const TabDataGrid = ({
const gridCol = safeColumnByName(gv, column.name);
if (!gridCol) continue;
const baseStyleName = column.styleName;
gridCol.styleCallback = (_g, dataCell) => {
gridCol.styleCallback = (grid, dataCell) => {
const dc = dataCell as {
itemIndex?: number;
index?: { itemIndex?: number };
@@ -303,16 +327,16 @@ const TabDataGrid = ({
const fieldName = dc?.dataField ?? dc?.fieldName ?? column.fieldName;
const data = rowDataMapRef.current.get(tabKey) || [];
if (itemIndex < 0 || itemIndex >= data.length) {
return baseStyleName ?? "";
return mergeCellStyleCallbackResult(baseStyleName, undefined);
}
const row = data[itemIndex] as Record<string, string | number>;
const extra = cellStyleCallbackRef.current?.({
rowIndex: itemIndex,
fieldName,
row,
grid: grid as CellStyleCallbackParams["grid"],
});
if (!extra) return baseStyleName ?? "";
return baseStyleName ? `${extra} ${baseStyleName}` : extra;
return mergeCellStyleCallbackResult(baseStyleName, extra);
};
attachedCount += 1;
}
@@ -373,20 +397,6 @@ const TabDataGrid = ({
const columns = columnInfoItem.columns || [];
const columnLayout = columnInfoItem.columnLayout || [];
// 엑셀 import 시 매칭을 위해 헤더에 fieldName 포함 (예: "합계 (swgeProcZoneInnerSm)")
const originalHeaders: Map<string, string> = new Map();
columns.forEach((col) => {
if (col.fieldName && gridView.columnByName) {
const gridCol = gridView.columnByName(col.name);
if (gridCol && gridCol.header) {
const originalText = gridCol.header.text || '';
originalHeaders.set(col.name, originalText);
const headerWithFieldName = `${originalText} (${col.fieldName})`;
gridCol.header.text = headerWithFieldName;
}
}
});
// exportGrid 호출 전에 columnLayout 정규화 (col 속성 추가)
const normalizeLayoutForExport = (layout: ColumnLayoutItem[]): ColumnLayoutItem[] => {
return layout.map(item => {
@@ -452,15 +462,6 @@ const TabDataGrid = ({
if (excelExportVisibilitySnapshot) {
restoreGridColumnsAfterExcelExport(gridView, excelExportVisibilitySnapshot);
}
// 헤더 원복
columns.forEach((col) => {
if (col.fieldName && gridView.columnByName) {
const gridCol = gridView.columnByName(col.name);
if (gridCol && gridCol.header && originalHeaders.has(col.name)) {
gridCol.header.text = originalHeaders.get(col.name) || '';
}
}
});
// columnLayout 원복 (footerUserSpans 유지)
if (normalizedLayout && !layoutRestored) {
layoutRestored = true;
@@ -546,29 +547,12 @@ const TabDataGrid = ({
const maxHeaderRowIndex = headerRowIndexes.length > 0 ? Math.max(...headerRowIndexes) : headerStartRow;
const dataStartRow = maxHeaderRowIndex + 1;
// 엑셀 컬럼 인덱스 → 그리드 fieldName 매핑
const headerToFieldNameMap = new Map<string, string>();
const columns = columnInfoItem.columns || [];
headers.forEach((headerText, colIndex) => {
let fieldName: string | null = null;
// 헤더에 fieldName이 포함된 경우 (예: "합계 (swgeProcZoneInnerSm)")
const fieldNameMatch = headerText.match(/\(([a-zA-Z_][a-zA-Z0-9_]*)\)$/);
if (fieldNameMatch && fieldNameMatch[1]) {
const extractedFieldName = fieldNameMatch[1];
if (columns.find(c => c.fieldName === extractedFieldName)) {
fieldName = extractedFieldName;
}
}
// 매칭 실패 시 원본 헤더 텍스트 사용
if (!fieldName) {
console.warn(`엑셀 컬럼 ${colIndex + 1} (헤더: "${headerText}") 매칭 실패 → 헤더 텍스트를 fieldName으로 사용`);
fieldName = headerText;
}
headerToFieldNameMap.set(headerText, fieldName);
});
const columnLayout = columnInfoItem.columnLayout || [];
const excelColumnFieldNames = getExcelImportColumnFieldNames(
columns,
columnLayout,
);
// 데이터 행 읽기
const dataRows: Record<string, string | number>[] = [];
@@ -579,9 +563,8 @@ const TabDataGrid = ({
for (let colIndex = 0; colIndex < headers.length; colIndex++) {
const col = range.s.c + colIndex;
const cell = worksheet[XLSX.utils.encode_cell({ r: row, c: col })];
const headerName = headers[colIndex] || `Column${colIndex + 1}`;
// 헤더 텍스트를 fieldName으로 변환
const fieldName = headerToFieldNameMap.get(headerName) || headerName;
const fieldName = excelColumnFieldNames[colIndex];
if (!fieldName) continue;
let cellValue: string | number = '';
if (cell && cell.v !== undefined && cell.v !== null) {
@@ -92,13 +92,12 @@ const verificationSubmitResultColumnInfo: ColumnInfo = {
{ name: "vrfcMthdNm", fieldName: "vrfcMthdNm", header: { text: "구분" }, editable: false },
{ name: "vrfcMthdCd", fieldName: "vrfcMthdCd", header: { text: "검증유형코드" }, editable: false, visible: false },
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역" }, editable: false },
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
{ name: "compVlCn", fieldName: "compVlCn", header: { text: "기준값" }, editable: false },
{ name: "prcsYn", fieldName: "prcsYn", header: { text: "처리여부" }, editable: false },
{ name: "pbcpAprvYn", fieldName: "pbcpAprvYn", header: { text: "공단승인여부" }, editable: false },
{ name: "regDt", fieldName: "regDt", header: { text: "등록일시" }, editable: false },
{ name: "mdfcnDt", fieldName: "mdfcnDt", header: { text: "갱신일시" }, editable: false },
{
@@ -111,13 +110,12 @@ const verificationSubmitResultColumnInfo: ColumnInfo = {
columnLayout: [
"vrfcMthdNm",
"ctpvNm",
"sggNm",
"admdstNm",
"vrfcTrgtNm",
"rsnExpln",
"nowVlCn",
"compVlCn",
"prcsYn",
"pbcpAprvYn",
"regDt",
"mdfcnDt",
"prcsOpnnExpln",
@@ -202,13 +200,12 @@ export function VerificationResultDialog({
vrfcMthdCd,
vrfcMthdNm: VRFC_MTHD_NAME_MAP[vrfcMthdCd] ?? item.vrfcMthdNm,
ctpvNm: item.ctpvNm,
sggNm: item.sggNm,
admdstNm: item.admdstNm ?? item.sggNm ?? "-",
vrfcTrgtNm: item.vrfcTrgtNm,
rsnExpln: item.rsnExpln,
nowVlCn: item.nowVlCn ?? "-",
compVlCn: item.compVlCn ?? "-",
prcsYn: PRCS_YN_LABEL[item.prcsYn] ?? item.prcsYn,
pbcpAprvYn: item.pbcpAprvYn,
prcsYn: item.prcsYnNm ?? PRCS_YN_LABEL[item.prcsYn] ?? item.prcsYn,
regDt: (item.regDt ?? "").slice(0, 16),
mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16),
prcsOpnnExpln: item.prcsOpnnExpln ?? "",
@@ -226,9 +223,9 @@ export function VerificationResultDialog({
sggNm: stableQueryContext.sggNm,
statsYr: stableQueryContext.statsYr,
statsCd: stableQueryContext.statsCd,
pbcpAprvYn: "N",
vrfcMthdCd,
prcsYnList: "0",
rejectCodeList: "0,1,2",
page: page + 1,
size: 10000,
};
@@ -137,12 +137,15 @@ function verificationPrcsYnNmCellStyle({ fieldName, row }: CellStyleCallbackPara
const verificationResultColumnInfo: ColumnInfo = {
columns: [
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역" }, editable: false },
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
{ name: "compVlCn", fieldName: "compVlCn", header: { text: "기준값" }, editable: false },
{ name: "prcsYnNm", fieldName: "prcsYnNm", header: { text: "처리여부" }, editable: false },
{ name: "stVrfRjtComNm", fieldName: "stVrfRjtComNm", header: { text: "반려상태" }, editable: false },
{ name: "prcsOpnnExpln", fieldName: "prcsOpnnExpln", header: { text: "처리의견" }, editable: false },
{ name: "rjctOpnnCn", fieldName: "rjctOpnnCn", header: { text: "반려의견" }, editable: false },
{ name: "regDt", fieldName: "regDt", header: { text: "등록일시" }, editable: false },
{ name: "mdfcnDt", fieldName: "mdfcnDt", header: { text: "갱신일시" }, editable: false },
{ name: "prcsYn", fieldName: "prcsYn", header: { text: "검증결과상태코드" }, editable: false, visible: false },
@@ -150,12 +153,15 @@ const verificationResultColumnInfo: ColumnInfo = {
],
columnLayout: [
"ctpvNm",
"sggNm",
"admdstNm",
"vrfcTrgtNm",
"rsnExpln",
"nowVlCn",
"compVlCn",
"prcsYnNm",
"stVrfRjtComNm",
"prcsOpnnExpln",
"rjctOpnnCn",
"regDt",
"mdfcnDt",
"prcsYn",
@@ -175,6 +181,8 @@ export interface VerificationResultHistoryDialogProps {
} | null;
title?: string;
emptyMessage?: string;
/** 예외 제출 취소 API 성공 후 부모(입력 화면)에서 메인 데이터를 다시 조회할 때 사용 */
onExceptionResetSubmitted?: () => void | Promise<void>;
}
export function VerificationResultHistoryDialog({
@@ -183,6 +191,7 @@ export function VerificationResultHistoryDialog({
queryContext,
title = "검증 결과 내역",
emptyMessage = "표시할 검증 결과가 없습니다.",
onExceptionResetSubmitted,
}: VerificationResultHistoryDialogProps) {
const stableQueryContext = useMemo(() => {
if (!queryContext) return null;
@@ -225,6 +234,7 @@ export function VerificationResultHistoryDialog({
};
const checkedVrfcByTabRef = useRef(checkedVrfcByTab);
checkedVrfcByTabRef.current = checkedVrfcByTab;
const loadedTabKeysRef = useRef<Set<VerificationHistoryTabKey>>(new Set());
const resetState = useCallback(() => {
setActiveTab("NV");
@@ -234,6 +244,7 @@ export function VerificationResultHistoryDialog({
setErrorMessage(null);
setResetSubmitting(false);
setCheckedVrfcByTab(createEmptyCheckedVrfcByTab());
loadedTabKeysRef.current.clear();
}, []);
const fetchVerificationResultPage = useCallback(
@@ -245,9 +256,9 @@ export function VerificationResultHistoryDialog({
sggNm: stableQueryContext.sggNm,
statsYr: stableQueryContext.statsYr,
statsCd: stableQueryContext.statsCd,
pbcpAprvYn: "N",
vrfcMthdCd,
prcsYnList: "0,2,3",
prcsYnList: "0,2",
rejectCodeList: "0,1,2",
page: pageIndex + 1,
size: PAGE_SIZE,
};
@@ -270,10 +281,10 @@ export function VerificationResultHistoryDialog({
);
const loadTabPage = useCallback(
async (tab: VerificationHistoryTabKey, pageIndex: number) => {
async (tab: VerificationHistoryTabKey, pageIndex: number): Promise<boolean> => {
if (!stableQueryContext) {
setErrorMessage("조회 조건이 없어 검증 결과를 불러올 수 없습니다.");
return;
return false;
}
setErrorMessage(null);
setLoadingByTab((prev) => ({ ...prev, [tab]: true }));
@@ -287,10 +298,12 @@ export function VerificationResultHistoryDialog({
? Math.max(0, Number(apiCurrentPage) - 1)
: pageIndex;
applyPageResponse(tab, resolvedPageIndex, list, totalPages);
return true;
} catch {
const tabLabel =
verificationHistoryTabs.find((t) => t.value === tab)?.label ?? tab;
setErrorMessage(`${tabLabel} 조회 중 오류가 발생했습니다.`);
return false;
} finally {
setLoadingByTab((prev) => ({ ...prev, [tab]: false }));
}
@@ -305,40 +318,27 @@ export function VerificationResultHistoryDialog({
}
setErrorMessage(null);
setTabPages(initialTabPageState());
setLoadingByTab({ NV: true, IMR: true, HB: true });
try {
const results = await Promise.all(
verificationHistoryTabs.map((tab) =>
fetchVerificationResultPage(tab.value, 0),
),
);
setEntriesByTab({
NV: results[0]?.data?.verifyResultList ?? [],
IMR: results[1]?.data?.verifyResultList ?? [],
HB: results[2]?.data?.verifyResultList ?? [],
});
setTabPages({
NV: {
pageIndex: 0,
pageCount: Math.max(results[0]?.data?.pageInfo?.totalPages ?? 1, 1),
},
IMR: {
pageIndex: 0,
pageCount: Math.max(results[1]?.data?.pageInfo?.totalPages ?? 1, 1),
},
HB: {
pageIndex: 0,
pageCount: Math.max(results[2]?.data?.pageInfo?.totalPages ?? 1, 1),
},
});
} catch {
setEntriesByTab({ NV: [], IMR: [], HB: [] });
setTabPages(initialTabPageState());
setErrorMessage("검증 결과 내역 조회 중 오류가 발생했습니다.");
} finally {
setLoadingByTab({ NV: false, IMR: false, HB: false });
setEntriesByTab({ NV: [], IMR: [], HB: [] });
loadedTabKeysRef.current.clear();
const loaded = await loadTabPage("NV", 0);
if (loaded) {
loadedTabKeysRef.current.add("NV");
}
}, [fetchVerificationResultPage, stableQueryContext]);
}, [loadTabPage, stableQueryContext]);
const handleActiveTabChange = useCallback(
(tab: VerificationHistoryTabKey) => {
setActiveTab(tab);
if (loadedTabKeysRef.current.has(tab)) return;
void (async () => {
const loaded = await loadTabPage(tab, 0);
if (loaded) {
loadedTabKeysRef.current.add(tab);
}
})();
},
[loadTabPage],
);
const handleTabPageChange = useCallback(
(tab: VerificationHistoryTabKey, pageIndex: number) => {
@@ -376,14 +376,16 @@ export function VerificationResultHistoryDialog({
vrfcSn: item.vrfcSn,
__checked: isChecked ? 1 : 0,
ctpvNm: item.ctpvNm,
sggNm: item.sggNm,
admdstNm: item.admdstNm ?? item.sggNm ?? "-",
vrfcTrgtNm: item.vrfcTrgtNm,
rsnExpln: item.rsnExpln,
nowVlCn: item.nowVlCn ?? "-",
compVlCn: item.compVlCn ?? "-",
prcsYnNm: item.prcsYnNm ?? "-",
stVrfRjtComNm: item.stVrfRjtComNm ?? "-",
prcsOpnnExpln: item.prcsOpnnExpln ?? "-",
rjctOpnnCn: item.rjctOpnnCn ?? "-",
prcsYn: item.prcsYn ?? "",
pbcpAprvYn: item.pbcpAprvYn,
regDt: (item.regDt ?? "").slice(0, 16),
mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16),
};
@@ -451,12 +453,28 @@ export function VerificationResultHistoryDialog({
alert("예외 제출 취소 처리되었습니다.");
setCheckedVrfcByTab((prev) => ({ ...prev, [tab]: new Map() }));
await loadTabPage(tab, tabPages[tab].pageIndex);
try {
await onExceptionResetSubmitted?.();
} catch {
alert(
"예외 제출 취소는 완료되었으나 화면 데이터를 다시 불러오지 못했습니다. 조회로 새로고침 해주세요.",
);
}
} catch {
alert("예외 제출 취소 처리 중 오류가 발생했습니다.");
} finally {
setResetSubmitting(false);
}
}, [activeTab, alert, checkedVrfcByTab, entriesByTab, loadTabPage, stableQueryContext, tabPages]);
}, [
activeTab,
alert,
checkedVrfcByTab,
entriesByTab,
loadTabPage,
onExceptionResetSubmitted,
stableQueryContext,
tabPages,
]);
const renderActiveTabPanel = (tab: VerificationHistoryTabKey) => {
const entries = entriesByTab[tab];
@@ -550,7 +568,7 @@ export function VerificationResultHistoryDialog({
)}
<Tabs
value={activeTab}
onValueChange={(value) => setActiveTab(value as VerificationHistoryTabKey)}
onValueChange={(value) => handleActiveTabChange(value as VerificationHistoryTabKey)}
className="w-full"
variant="default"
>
@@ -34,8 +34,8 @@ export async function hasAnyPostSubmitVerificationResult(
sggNm: ctx.sggNm,
statsYr: ctx.statsYr,
statsCd: ctx.statsCd,
pbcpAprvYn: "N" as const,
prcsYnList: "0",
rejectCodeList: "0,1,2",
page: 1,
size: 1,
};
@@ -1,6 +1,6 @@
"use client";
import { useCallback, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import {
type InfiniteData,
keepPreviousData,
@@ -9,12 +9,12 @@ import {
} from "@tanstack/react-query";
import { useTabStore as useGridTabStore } from "@rsone/components";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { verificationInputReportApi } from "@/entities/statistics/api/verification-input-report-service";
import type {
VerificationResultListRequest,
VerificationResultListResponse,
VerificationResultReportItem,
} from "@/entities/statistics/model/statistics-interface";
VerificationInputReportItem,
VerificationInputReportResponse,
VerificationInputReportTypeCountingInfo,
} from "@/entities/statistics/model/verification-input-report-interface";
import { useAlertStore, useMenuStore } from "@/shared/lib";
import {
findMenuItemByUrlPath,
@@ -59,8 +59,8 @@ const TYPE_CODE_NAME_MAP: Record<string, string> = {
const PAGE_SIZE = 100;
const SCROLL_LOAD_THRESHOLD_PX = 120;
function getNextVerificationResultPage(
lastPage: VerificationResultListResponse,
function getNextVerificationInputReportPage(
lastPage: VerificationInputReportResponse,
): number | undefined {
const pageInfo = lastPage?.data?.pageInfo;
if (!pageInfo) return undefined;
@@ -69,19 +69,29 @@ function getNextVerificationResultPage(
return undefined;
}
function flattenVerificationResults(
data: InfiniteData<VerificationResultListResponse> | undefined,
): VerificationResultReportItem[] {
function flattenVerificationInputReportResults(
data: InfiniteData<VerificationInputReportResponse> | undefined,
): VerificationInputReportItem[] {
return (
data?.pages.flatMap((page) => page?.data?.verifyResultList ?? []) ?? []
);
}
function getVerificationResultTotalCount(
data: InfiniteData<VerificationResultListResponse> | undefined,
loadedCount: number,
): number {
return data?.pages[0]?.data?.pageInfo?.totalElements ?? loadedCount;
function getVerifyTypeCountingInfo(
data: InfiniteData<VerificationInputReportResponse> | undefined,
): VerificationInputReportTypeCountingInfo | null {
return data?.pages[0]?.data?.verifyTypeCountingInfo ?? null;
}
function formatReportItemTitle(item: VerificationInputReportItem): string {
return item.statsNm?.trim() || item.vrfcTrgtNm?.trim() || item.statsCd;
}
function formatReportItemRegion(item: VerificationInputReportItem): string {
const parts = [item.ctpvNm, item.sggNm].filter(
(v): v is string => Boolean(v?.trim()),
);
return parts.length > 0 ? parts.join(" ") : "-";
}
function useVerificationReportList(
@@ -89,28 +99,31 @@ function useVerificationReportList(
statsCdQueryKey: string,
vrfcMthdCd: VerificationReportTabKey,
statsCd: string,
enabled: boolean,
) {
return useInfiniteQuery({
queryKey: [
"verificationResultList",
"verificationInputReport",
statsYr,
vrfcMthdCd,
statsCdQueryKey,
],
initialPageParam: 1,
queryFn: ({ pageParam }) =>
statisticsApi.getVerificationResultList({
verificationInputReportApi.getVerificationInputReport({
statsYr,
pbcpAprvYn: "N",
vrfcMthdCd,
page: pageParam,
size: PAGE_SIZE,
prcsYnList: "0",
vrfcMthdCd,
...(!(statsCd === "" || statsCd === "ALL") && { statsCd }),
} satisfies VerificationResultListRequest),
getNextPageParam: getNextVerificationResultPage,
}),
getNextPageParam: getNextVerificationInputReportPage,
placeholderData: keepPreviousData,
refetchOnWindowFocus: false,
// 탭 전환만으로는 재조회하지 않음(명시적 새로고침/조건 변경 시만 조회)
staleTime: Infinity,
refetchOnMount: false,
enabled,
});
}
@@ -132,28 +145,46 @@ const VerificationSelectionReport = ({
const alert = useAlertStore((s) => s.setMessage);
const [activeTab, setActiveTab] = useState<VerificationReportTabKey>("NV");
const [statsCd, setStatsCd] = useState<string>("ALL");
const [isFullReloading, setIsFullReloading] = useState(false);
const statsYr = String(dayjs().year() - 1);
const statsCdQueryKey =
statsCd === "" || statsCd === "ALL" ? "" : statsCd;
const handleStatsCdChange = useCallback(
(nextStatsCd: string) => {
setStatsCd(nextStatsCd);
setActiveTab("NV");
setIsFullReloading(true);
// 통계항목 변경 시 모든 탭 캐시 제거 후 NV부터 다시 조회
void queryClient.removeQueries({
queryKey: ["verificationInputReport", statsYr],
});
},
[queryClient, statsYr],
);
const nvQuery = useVerificationReportList(
statsYr,
statsCdQueryKey,
"NV",
statsCd,
activeTab === "NV",
);
const imrQuery = useVerificationReportList(
statsYr,
statsCdQueryKey,
"IMR",
statsCd,
activeTab === "IMR",
);
const hbQuery = useVerificationReportList(
statsYr,
statsCdQueryKey,
"HB",
statsCd,
activeTab === "HB",
);
const queryByTab = {
@@ -162,44 +193,51 @@ const VerificationSelectionReport = ({
HB: hbQuery,
} as const;
const isInitialLoading =
nvQuery.isPending || imrQuery.isPending || hbQuery.isPending;
const isRefreshing =
(nvQuery.isFetching && !nvQuery.isPending) ||
(imrQuery.isFetching && !imrQuery.isPending) ||
(hbQuery.isFetching && !hbQuery.isPending);
const error = nvQuery.error ?? imrQuery.error ?? hbQuery.error;
const activeQuery = queryByTab[activeTab];
const isRefreshing = activeQuery.isFetching && !activeQuery.isPending;
const error = activeQuery.error;
/** 통계항목 변경·최초 진입: 탭 영역 숨기고 카드 전체 로딩만 표시 */
const hideTabsSection =
isFullReloading ||
(activeTab === "NV" && !nvQuery.data && (nvQuery.isPending || nvQuery.isFetching));
useEffect(() => {
if (!isFullReloading) return;
if (nvQuery.isFetching) return;
if (nvQuery.isSuccess || nvQuery.isError) {
setIsFullReloading(false);
}
}, [isFullReloading, nvQuery.isFetching, nvQuery.isSuccess, nvQuery.isError]);
const handleRefresh = useCallback(() => {
const tabCodes: VerificationReportTabKey[] = ["NV", "IMR", "HB"];
void Promise.all(
tabCodes.map((code) =>
queryClient.resetQueries({
queryKey: [
"verificationResultList",
statsYr,
code,
statsCdQueryKey,
],
}),
),
);
}, [queryClient, statsYr, statsCdQueryKey]);
setStatsCd("ALL");
setActiveTab("NV");
setIsFullReloading(true);
void queryClient.removeQueries({
queryKey: ["verificationInputReport", statsYr],
});
}, [queryClient, statsYr]);
const listsByTab: Record<VerificationReportTabKey, VerificationResultReportItem[]> = {
NV: flattenVerificationResults(nvQuery.data),
IMR: flattenVerificationResults(imrQuery.data),
HB: flattenVerificationResults(hbQuery.data),
const listsByTab: Record<VerificationReportTabKey, VerificationInputReportItem[]> = {
NV: flattenVerificationInputReportResults(nvQuery.data),
IMR: flattenVerificationInputReportResults(imrQuery.data),
HB: flattenVerificationInputReportResults(hbQuery.data),
};
const typeCountingInfo =
getVerifyTypeCountingInfo(nvQuery.data) ??
getVerifyTypeCountingInfo(imrQuery.data) ??
getVerifyTypeCountingInfo(hbQuery.data);
const totalCountByTab: Record<VerificationReportTabKey, number> = {
NV: getVerificationResultTotalCount(nvQuery.data, listsByTab.NV.length),
IMR: getVerificationResultTotalCount(imrQuery.data, listsByTab.IMR.length),
HB: getVerificationResultTotalCount(hbQuery.data, listsByTab.HB.length),
NV: typeCountingInfo?.nvCount ?? listsByTab.NV.length,
IMR: typeCountingInfo?.imrCount ?? listsByTab.IMR.length,
HB: typeCountingInfo?.hbCount ?? listsByTab.HB.length,
};
const openInputFromStatsCd = useCallback(
(item: VerificationResultReportItem) => {
(item: VerificationInputReportItem) => {
const nav = resolveInputPageNavigationFromStatsCd(item.statsCd);
if (!nav) {
alert(
@@ -263,9 +301,21 @@ const VerificationSelectionReport = ({
[],
);
const isTabContentLoading = (tab: VerificationReportTabKey) => {
const query = queryByTab[tab];
return (
activeTab === tab &&
!hideTabsSection &&
(query.isPending ||
(query.isFetching && !query.isFetchingNextPage) ||
(!query.data && !query.error))
);
};
const renderResultList = (
typeCode: VerificationReportTabKey,
items: VerificationResultReportItem[],
items: VerificationInputReportItem[],
isTabLoading: boolean,
hasNextPage: boolean,
isFetchingNextPage: boolean,
fetchNextPage: () => void,
@@ -281,7 +331,13 @@ const VerificationSelectionReport = ({
)
}
>
{items.length === 0 && !isFetchingNextPage ? (
{isTabLoading ? (
<div className="flex h-full min-h-[8rem] items-center justify-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
) : items.length === 0 && !isFetchingNextPage ? (
<div className="flex h-full min-h-[8rem] items-center justify-center text-lg text-muted-foreground">
항목이 없습니다.
</div>
@@ -303,7 +359,7 @@ const VerificationSelectionReport = ({
>
<div className="mb-2 flex items-center justify-between">
<span className="font-medium">
{item.statsNm}
{formatReportItemTitle(item)}
</span>
<Badge
color={
@@ -320,7 +376,7 @@ const VerificationSelectionReport = ({
{item.rsnExpln}
</p>
<div className="flex justify-between text-lg text-muted-foreground">
<span>{item.admdstNm}</span>
<span>{formatReportItemRegion(item)}</span>
<span>
현재: {item.nowVlCn ?? "-"} / 비교:{" "}
{item.compVlCn ?? "-"}
@@ -383,25 +439,25 @@ const VerificationSelectionReport = ({
<CodeSelectWithAll
upCd="STITEM"
value={statsCd}
onValueChange={setStatsCd}
onValueChange={handleStatsCdChange}
placeholder="전체"
className="w-full"
/>
</div>
<div className="card-body p-2.5 flex-1 flex flex-col overflow-hidden relative min-h-0">
{isInitialLoading && (
{hideTabsSection && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
{error && (
{!hideTabsSection && error && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
데이터를 불러올 수 없습니다.
</div>
)}
{!isInitialLoading && !error && (
{!hideTabsSection && !error && (
<section className="flex min-h-0 flex-1 flex-col">
<Tabs
value={activeTab}
@@ -428,18 +484,14 @@ const VerificationSelectionReport = ({
value={tab.value}
className="mt-0 flex min-h-0 flex-1 flex-col p-0 data-[state=inactive]:hidden"
>
{activeTab === tab.value
? renderResultList(
tab.value,
listsByTab[tab.value],
queryByTab[tab.value]
.hasNextPage ?? false,
queryByTab[tab.value]
.isFetchingNextPage,
queryByTab[tab.value]
.fetchNextPage,
)
: null}
{renderResultList(
tab.value,
listsByTab[tab.value],
isTabContentLoading(tab.value),
queryByTab[tab.value].hasNextPage ?? false,
queryByTab[tab.value].isFetchingNextPage,
queryByTab[tab.value].fetchNextPage,
)}
</TabsContent>
))}
</Tabs>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,8 +1,10 @@
import { useEffect, useRef, useState } from 'react';
import { useEffect, useRef, useState, type Dispatch, type SetStateAction } from 'react';
import { useConfirm } from '@frontend-opensource/use-react-hooks';
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from '@/shared/lib';
import { STATISTICS_INPUT_URL_PATH } from '@/shared/lib/stats-cd-input-navigation';
import { useStatisticsInputPendingNavigation } from '@/shared/lib/use-statistics-input-pending-navigation';
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';
@@ -46,6 +48,7 @@ import type { SearchFormValues } from '@/components/type/SearchForm.type';
import { doList } from '@/components/util/column/input/DoList';
import { PublicSewageFactInpColumnList } from '@/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactInpColumn';
import { toGridRows } from '@/components/util/grid/gridFunction';
import { patchVerificationFailedByTab } from '@/components/util/input/verification-failed-flag';
import { useAdminStore } from '@/store/admin-store';
type GridRowWithVerification = Record<string, string | number> & {
@@ -157,12 +160,17 @@ const PublicSewageFacilityInp = () => {
const globalProgressVisible = useGlobalProgressStore((state) => state.visible);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [hasSearched, setHasSearched] = useState(false);
const [isNotApplicable, setIsNotApplicable] = useState(false);
const [isNotApplicableByTab, setIsNotApplicableByTab] = useState<Map<string, boolean>>(
() => new Map(),
);
const [showIsNotApplicable, setShowIsNotApplicable] = useState(false);
const [searchValues, setSearchValues] = useState<SearchFormValues | null>(null);
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
const [tabStatusInfoMap, setTabStatusInfoMap] = useState<Map<string, SewerageStatusInfo | null>>(new Map());
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean>
>(new Map());
/** 탭키별 `engColNm -> columnGuide` 캐시 */
const [columnGuidesByTab, setColumnGuidesByTab] = useState<Record<string, Record<string, string>>>({});
const [_toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<Map<string, TabToolbarSnapshot>>(() => new Map());
@@ -175,6 +183,19 @@ const PublicSewageFacilityInp = () => {
const [mainFacilityMappingSaveDialogKey, setMainFacilityMappingSaveDialogKey] = useState(0);
const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false);
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
const isNotApplicable = isNotApplicableByTab.get('ST00060001') ?? false;
const setIsNotApplicable: Dispatch<SetStateAction<boolean>> = (next) => {
setIsNotApplicableByTab((prev) => {
const current = prev.get('ST00060001') ?? false;
const resolved =
typeof next === 'function'
? (next as (prevState: boolean) => boolean)(current)
: next;
const nextMap = new Map(prev);
nextMap.set('ST00060001', resolved);
return nextMap;
});
};
const periodEnterableRef = useRef(true);
const searchDoneRef = useRef(false);
@@ -195,7 +216,7 @@ const PublicSewageFacilityInp = () => {
const currentData =
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
if (!treatAsNoApplicable && (!currentData || currentData.length === 0)) {
alert('제출할 데이터가 없습니다.');
alert('해당사항 없음을 체크해주세요.');
return;
}
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
@@ -276,15 +297,15 @@ const PublicSewageFacilityInp = () => {
const currentData =
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
if (!treatAsNoApplicable && (!currentData || currentData.length === 0)) {
alert('제출할 데이터가 없습니다.');
const submitList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData);
if (!treatAsNoApplicable && submitList.length === 0) {
alert('해당사항 없음을 체크해주세요.');
return;
}
try {
showGlobalProgress({ label: '서버에 요청 중...', progress: 0 });
const userInfo = await getUserInfo();
const submitList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData);
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
const statsCdForTab = tabStatsCdMap.get(activeTab) ?? activeTab;
const requestData = {
@@ -515,19 +536,19 @@ const PublicSewageFacilityInp = () => {
if (!(await confirm('저장 하시겠습니까?'))) return;
if (activeTab === 'ST00060001') {
const noApplicable = getNoApplicableForRequest();
const treatAsNoApplicable = noApplicable || isNotApplicable;
const currentData =
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
const saveList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData);
if (!treatAsNoApplicable && saveList.length === 0) {
alert('해당사항 없음을 체크해주세요.');
return;
}
try {
showGlobalProgress({ label: '저장 진행 중...', progress: 0 });
const userInfo = await getUserInfo();
const noApplicable = getNoApplicableForRequest();
const treatAsNoApplicable = noApplicable || isNotApplicable;
const currentData =
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
if (!treatAsNoApplicable && (!currentData || currentData.length === 0)) {
alert('저장할 데이터가 없습니다.');
return;
}
const saveList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData);
const param: PublicSewageFacilitySaveRequest = {
statsYr: lastSearchedValues.year ?? '',
ctpvNm: lastSearchedValues.sido ?? '',
@@ -567,11 +588,6 @@ const PublicSewageFacilityInp = () => {
})();
};
const updateOperateDataButtonFunc = () => {
console.log('updatedOperateData');
// 운영 데이터 업데이트 로직
};
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
const [activeTab, setActiveTab] = useState<string>(
PublicSewageFactInpColumnList[0]?.tabKey ?? 'ST00060001',
@@ -655,8 +671,6 @@ const PublicSewageFacilityInp = () => {
param: PublicSewageFacilityRequestParams,
searchContext: SearchFormValues | null,
): Promise<boolean> => {
// 서버 데이터로 새로 불러오는 시점이므로 "해당없음" 체크는 항상 초기화
setIsNotApplicable(false);
try {
if (tabKey === 'ST00060001') {
const response = await statisticsInputPublicSewageFacilityApiService.getPublicSewageFacility(param);
@@ -677,8 +691,10 @@ const PublicSewageFacilityInp = () => {
if (list.length === 0) {
setShowIsNotApplicable(true);
setIsNotApplicable(true);
} else {
setShowIsNotApplicable(false);
setIsNotApplicable(false);
}
const gridRows = toGridRows(convertedData);
@@ -692,6 +708,9 @@ const PublicSewageFacilityInp = () => {
next.set(tabKey, statsCd);
return next;
});
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setRowDataMap((prev) => {
const next = new Map(prev);
next.set('ST00060001', gridRows);
@@ -736,6 +755,9 @@ const PublicSewageFacilityInp = () => {
next.set(tabKey, statsCd);
return next;
});
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, response.data.commonInfo),
);
setRowDataMap((prev) => {
const next = new Map(prev);
next.set('MainFacility', leftGridRows);
@@ -753,6 +775,52 @@ const PublicSewageFacilityInp = () => {
}
};
const updateOperateDataButtonFunc = () => {
void (async () => {
if (!lastSearchedValues) {
alert('먼저 조회를 진행해주세요.');
return;
}
if (!(await confirm('운영 데이터를 업데이트 하시겠습니까?'))) return;
try {
showGlobalProgress({ label: '운영 데이터 업데이트 중...', progress: 0 });
const userInfo = await getUserInfo();
const request = {
statsYr: lastSearchedValues.year ?? '',
ctpvNm: lastSearchedValues.sido ?? '',
sggNm: lastSearchedValues.gugun === 'ALL' ? '' : lastSearchedValues.gugun ?? '',
sewerBzmnCd: lastSearchedValues.bsnmCd ?? '',
userId: userInfo.userUnqId,
};
const response =
await statisticsInputPublicSewageFacilityApiService.updatePublicSewageFacilityOperationData(
request,
);
if (response.status !== 'success') {
alert(response.message || '운영 데이터 업데이트 중 오류가 발생했습니다.');
return;
}
setGlobalProgress(50);
const param: PublicSewageFacilityRequestParams = {
statsYr: request.statsYr,
ctpvNm: request.ctpvNm,
sggNm: request.sggNm,
sewerBzmnCd: request.sewerBzmnCd,
};
await loadPublicSewageFacilityActiveTabData('ST00060001', param, lastSearchedValues);
setGlobalProgress(100);
alert('운영 데이터가 업데이트되었습니다.');
} catch {
alert('운영 데이터 업데이트 중 오류가 발생했습니다.');
} finally {
hideGlobalProgress();
}
})();
};
const runMainFacilityMappingSave = async (
draft: MainFacilityMappingSaveDraft,
dialogInput: MainFacilityMappingSaveDialogValues | null,
@@ -820,7 +888,10 @@ const PublicSewageFacilityInp = () => {
}
};
const handleSearch = async (values: SearchFormValues) => {
const handleSearch = async (
values: SearchFormValues,
opts?: { forcedTabKey?: string },
) => {
if (!values.sido || values.sido === 'ALL') {
alert('시도를 선택해주세요.');
return;
@@ -848,6 +919,7 @@ const PublicSewageFacilityInp = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setToolbarSnapshotByTab(new Map());
activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey);
@@ -860,7 +932,13 @@ const PublicSewageFacilityInp = () => {
setIsLoading(true);
const param = buildPublicSewageFacilityParams(values);
searchParamRef.current = param;
const tabKeyToLoad = yearChangedFromLastSearch ? firstTabKey : activeTabRef.current;
const tabKeyToLoad =
opts?.forcedTabKey ??
(yearChangedFromLastSearch ? firstTabKey : activeTabRef.current);
if (opts?.forcedTabKey) {
activeTabRef.current = opts.forcedTabKey;
setActiveTab(opts.forcedTabKey);
}
await loadPublicSewageFacilityActiveTabData(tabKeyToLoad, param, values);
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
@@ -870,6 +948,19 @@ const PublicSewageFacilityInp = () => {
};
const handleActiveTabChange = async (tabKey: string) => {
const prevTabKey = activeTabRef.current;
if (prevTabKey === 'ST00060001') {
const latestRowsInPrevTab =
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
if (latestRowsInPrevTab.length > 0 || rowDataMap.has('ST00060001')) {
setRowDataMap((prev) => {
const next = new Map(prev);
next.set('ST00060001', latestRowsInPrevTab);
return next;
});
}
}
activeTabRef.current = tabKey;
setActiveTab(tabKey);
@@ -885,12 +976,13 @@ const PublicSewageFacilityInp = () => {
const handleReset = () => {
setIsLoading(false);
setHasSearched(false);
setIsNotApplicable(false);
setIsNotApplicableByTab(new Map());
setShowIsNotApplicable(false);
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setToolbarSnapshotByTab(new Map());
setInputPeriodEnterable(true);
@@ -1066,10 +1158,23 @@ const PublicSewageFacilityInp = () => {
};
})();
const { verificationSearchDefaults, searchFormRemountKey } =
useStatisticsInputPendingNavigation({
urlPath: STATISTICS_INPUT_URL_PATH.PUBLIC_SEWAGE_FACILITY,
onPending: (pending) => {
activeTabRef.current = pending.tabKey;
setActiveTab(pending.tabKey);
},
onAutoSearch: (values, tabKey) =>
handleSearch(values, { forcedTabKey: tabKey }),
});
return (
<>
<div className="relative">
<SearchForm
key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined}
fctName={true}
onSubmit={handleSearch}
onReset={handleReset}
@@ -1081,6 +1186,9 @@ const PublicSewageFacilityInp = () => {
{!isMainFacilityTab && (
<VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog}
isVerificationFailed={
isVerificationFailedByTab.get(activeTab) === true
}
/>
)}
{showEnterableToolbarButtons && !isMainFacilityTab && (
@@ -1215,7 +1323,7 @@ const PublicSewageFacilityInp = () => {
return (
<TabsContent key={tab.tabKey} value={tab.tabKey}>
<DataGrid
key={(tabEnterableForGrid || isAdminInfo) ? 'enterable' : 'readonly'}
key={tab.tabKey}
columnInfo={gridColumnInfo}
rowStartNum={tab.rowStartNum}
excelExportFileSuffix={tab.tabLabel || tab.tabKey}
@@ -1278,6 +1386,11 @@ const PublicSewageFacilityInp = () => {
open={verificationHistoryOpen}
onOpenChange={setVerificationHistoryOpen}
queryContext={verificationQueryContext}
onExceptionResetSubmitted={async () => {
const ctx = lastSearchedValues ?? searchValues;
if (!ctx || !searchParamRef.current) return;
await loadPublicSewageFacilityActiveTabData(activeTab, searchParamRef.current, ctx);
}}
/>
<SubmitDailog
open={showDailog}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -69,6 +69,9 @@ import { PipeTypeStatusInpColumn } from "@/components/util/column/input/sewagePi
import { RegionalFactStatusInpColumn } from "@/components/util/column/input/sewagePipe/RegionalFactStatusInpColumn";
import { SewagePipeInpColumnInfoList } from "@/components/util/column/input/sewagePipe/SewagePipeInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import {
patchVerificationFailedByTab,
} from "@/components/util/input/verification-failed-flag";
import { useAdminStore } from "@/store/admin-store";
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
import { useTabStore } from "@/store/tab-store";
@@ -360,6 +363,9 @@ const SewagePipeInp = () => {
const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false);
const [statsCd, setStatsCd] = useState<string>('');
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean>
>(new Map());
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
const [periodCheckList, setPeriodCheckList] = useState<CheckInputPeriodItem[]>([]);
/** 탭키별 `engColNm -> columnGuide` 캐시 */
@@ -396,6 +402,8 @@ const SewagePipeInp = () => {
values: SearchFormValues;
tabKey: string;
} | null>(null);
/** ST00020002 무데이터 안내/자동이동은 동일 조회조건에서 1회만 수행 */
const factInstallNoDataHandledKeyRef = useRef<string | null>(null);
const [verificationSearchDefaults, setVerificationSearchDefaults] =
useState<SearchFormValues | null>(null);
@@ -468,6 +476,7 @@ const SewagePipeInp = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setLastSearchedYear(null);
setLastSearchedValues(null);
setHasSearched(false);
@@ -549,6 +558,12 @@ const SewagePipeInp = () => {
mergeIntoExistingMap: boolean,
updateLastSearchedValues: boolean,
): Promise<boolean> => {
const searchConditionKey = JSON.stringify({
statsYr: searchParam.statsYr ?? "",
ctpvNm: searchParam.ctpvNm ?? "",
sggNm: searchParam.sggNm ?? "",
sewerBzmnCd: searchParam.sewerBzmnCd ?? "",
});
const applyRowMap = (apply: (m: Map<string, Record<string, string | number>[]>) => void) => {
if (mergeIntoExistingMap) {
setRowDataMap((prev) => {
@@ -574,6 +589,9 @@ const SewagePipeInp = () => {
applyRowMap((m) => m.set('ST00020001', toGridRows(convertedData)));
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
commitToolbarSnapshotForLoadedTab(
@@ -595,9 +613,29 @@ const SewagePipeInp = () => {
ctpvNm: commonInfo.ctpvNm,
...d,
}));
if (convertedData.length === 0) {
const alreadyHandledForSameSearch =
factInstallNoDataHandledKeyRef.current === searchConditionKey;
if (!alreadyHandledForSameSearch) {
factInstallNoDataHandledKeyRef.current = searchConditionKey;
alert("시설별 설치 현황에 조회된 데이터가 없습니다.\n시설별 현황 관리에서 설정해주세요");
activeTabRef.current = "ST00020006";
setActiveTab("ST00020006");
return await loadSewagePipeActiveTabData(
"ST00020006",
searchParam,
values,
true,
updateLastSearchedValues,
);
}
}
applyRowMap((m) => m.set('ST00020002', toGridRows(convertedData)));
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
commitToolbarSnapshotForLoadedTab(
@@ -621,6 +659,9 @@ const SewagePipeInp = () => {
applyRowMap((m) => m.set('ST00020003', toGridRows(convertedData)));
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
commitToolbarSnapshotForLoadedTab(
@@ -645,6 +686,9 @@ const SewagePipeInp = () => {
applyRowMap((m) => m.set('ST00020004', toGridRows(convertedData)));
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
commitToolbarSnapshotForLoadedTab(
@@ -668,6 +712,9 @@ const SewagePipeInp = () => {
applyRowMap((m) => m.set('ST00020005', toGridRows(convertedData)));
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
commitToolbarSnapshotForLoadedTab(
@@ -698,6 +745,9 @@ const SewagePipeInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
commitToolbarSnapshotForLoadedTab(
@@ -749,6 +799,7 @@ const SewagePipeInp = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setStatsCd("");
setToolbarSnapshotByTab(new Map());
}
@@ -795,7 +846,39 @@ const SewagePipeInp = () => {
}, [searchFormRemountKey]);
const getCurrentRowsForTab = (tabKey: string): Record<string, string | number>[] => {
switch (tabKey) {
case REGIONAL_FACT_TAB_KEY:
return getGridDataRef.current?.() ?? [];
case "ST00020002":
return getFactInstallGridDataRef.current?.() ?? [];
case "ST00020003":
return getPipeTypeGridDataRef.current?.() ?? [];
case "ST00020004":
return getPipeDiameterGridDataRef.current?.() ?? [];
case "ST00020005":
return getManholeGridDataRef.current?.() ?? [];
case "ST00020006":
return [
...(rowDataMap.get("treatmentPlantConnectionInp") ?? []),
...(rowDataMap.get("adminAreaConnectionInp") ?? []),
];
default:
return rowDataMap.get(tabKey) ?? [];
}
};
const handleActiveTabChange = async (tabKey: string) => {
const prevTabKey = activeTabRef.current;
const latestRowsInPrevTab = getCurrentRowsForTab(prevTabKey);
if (latestRowsInPrevTab.length > 0 || rowDataMap.has(prevTabKey)) {
setRowDataMap((prev) => {
const next = new Map(prev);
next.set(prevTabKey, latestRowsInPrevTab);
return next;
});
}
activeTabRef.current = tabKey;
setActiveTab(tabKey);
setStatsCd(tabStatsCdMap.get(tabKey) ?? "");
@@ -1735,7 +1818,7 @@ const SewagePipeInp = () => {
const maxYy = getManholeLastDataYear() % 100;
const saveList = currentData.map((d) => {
const yearlyStats = [];
const yearlyStats: SeweragePipeInputManholeStatusSaveYearlyStatItem[] = [];
for (let yy = minYy; yy <= maxYy; yy++) {
const year = 2000 + yy;
const wholKey = `yy${yy}WholMnhlCnt`;
@@ -1839,6 +1922,9 @@ const SewagePipeInp = () => {
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
<VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog}
isVerificationFailed={
isVerificationFailedByTab.get(activeTab) === true
}
/>
{showEnterableToolbarButtons && (
<>
@@ -1978,7 +2064,7 @@ const SewagePipeInp = () => {
return (
<TabsContent key={tabKey} value={tabKey}>
<DataGrid
key={(tabEnterableForGrid || isAdminInfo) ? "enterable" : "readonly"}
key={tabKey}
columnInfo={gridColumnInfo}
rowStartNum={info.rowStartNum}
excelExportFileSuffix={info.tabLabel || tabKey}
@@ -2062,6 +2148,12 @@ const SewagePipeInp = () => {
}
: null
}
onExceptionResetSubmitted={async () => {
const values = lastSearchedValues ?? searchValues;
if (!values?.year || !values?.sido || !values?.bsnmCd) return;
const refreshedParam = buildSewagePipeSearchParam(values);
await loadSewagePipeActiveTabData(activeTab, refreshedParam, values, true, false);
}}
/>
<SubmitDailog
open={showDailog}
@@ -50,12 +50,13 @@ import SearchForm from "@/components/module/SearchForm";
import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
import type { ColumnConfig, ColumnInfo } from "@/components/type/DataGrid.type";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { doList } from "@/components/util/column/input/DoList";
import { PipeDredgingStatusInpColumn } from "@/components/util/column/input/sewagePipeMaintain/PipeDredgingStatusInpColumn";
import { RepairStatusInpColumn } from "@/components/util/column/input/sewagePipeMaintain/RepairStatusInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
import { useAdminStore } from "@/store/admin-store";
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
import { useTabStore } from "@/store/tab-store";
@@ -80,6 +81,12 @@ const SewagePipeMaintainInpTabList = [
const firstTabKey = RepairStatusInpColumn.tabKey;
const SEWAGE_PIPE_MAINTAIN_COLUMN_GUIDE_TAB_KEYS = Array.from(
new Set(
SewagePipeMaintainInpTabList.map((tab) => tab.tabKey).filter(Boolean),
),
);
const toGridColumnInfo = (tab: (typeof SewagePipeMaintainInpTabList)[number]): ColumnInfo => ({
columns: tab.columns ?? [],
columnLayout: tab.columnLayout ?? [],
@@ -285,6 +292,9 @@ const SewagePipeMaintainInp = () => {
Map<string, SewerageStatusInfo | null>
>(new Map());
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean>
>(new Map());
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
Map<string, TabToolbarSnapshot>
>(new Map());
@@ -302,6 +312,9 @@ const SewagePipeMaintainInp = () => {
const [rowDataMap, setRowDataMap] = useState<
Map<string, Record<string, string | number>[]>
>(new Map());
const [columnGuidesByTab, setColumnGuidesByTab] = useState<
Record<string, Record<string, string>>
>({});
const searchDoneRef = useRef(false);
const periodEnterableRef = useRef(true);
@@ -328,6 +341,37 @@ const SewagePipeMaintainInp = () => {
}
}, [activeTabId, inputFocusNonce]);
useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE_MAINTAIN) return;
let cancelled = false;
void (async () => {
const entries = await Promise.all(
SEWAGE_PIPE_MAINTAIN_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => {
try {
const res = await statisticsApi.getColumnGuides(tabKey);
if (res.status !== "success" || !Array.isArray(res.data)) {
return [tabKey, {}] as const;
}
const next: Record<string, string> = {};
for (const item of res.data) {
const key = item.engColNm?.trim();
const guide = item.columnGuide?.trim();
if (key && guide) next[key] = guide;
}
return [tabKey, next] as const;
} catch {
return [tabKey, {}] as const;
}
}),
);
if (cancelled) return;
setColumnGuidesByTab(Object.fromEntries(entries));
})();
return () => {
cancelled = true;
};
}, [activeTabId]);
const commitToolbarSnapshotForLoadedTab = (
tabKey: string,
statusInfo: SewerageStatusInfo | null,
@@ -401,6 +445,9 @@ const SewagePipeMaintainInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) {
setLastSearchedValues(searchContext);
}
@@ -434,6 +481,9 @@ const SewagePipeMaintainInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) {
setLastSearchedValues(searchContext);
}
@@ -572,7 +622,7 @@ const SewagePipeMaintainInp = () => {
tabKey,
searchParamRef.current!,
lastSearchedValues,
false,
true,
false,
);
};
@@ -725,7 +775,7 @@ const SewagePipeMaintainInp = () => {
RepairStatusInpColumn.tabKey,
searchParamRef.current,
lastSearchedValues,
false,
true,
false,
);
setGlobalProgress(100);
@@ -762,7 +812,7 @@ const SewagePipeMaintainInp = () => {
PipeDredgingStatusInpColumn.tabKey,
searchParamRef.current,
lastSearchedValues,
false,
true,
false,
);
setGlobalProgress(100);
@@ -811,7 +861,7 @@ const SewagePipeMaintainInp = () => {
tabKey,
searchParamRef.current,
lastSearchedValues,
false,
true,
false,
);
alert("제출 취소되었습니다.");
@@ -828,6 +878,7 @@ const SewagePipeMaintainInp = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setToolbarSnapshotByTab(new Map());
setActiveTab(firstTabKey);
setLastSearchedValues(null);
@@ -872,6 +923,7 @@ const SewagePipeMaintainInp = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setToolbarSnapshotByTab(new Map());
activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey);
@@ -912,6 +964,16 @@ const SewagePipeMaintainInp = () => {
}, [searchFormRemountKey]);
const handleActiveTabChange = async (tabKey: string) => {
const prevTabKey = activeTabRef.current;
const latestRowsInPrevTab = getTabGridRows(prevTabKey);
if (latestRowsInPrevTab.length > 0 || rowDataMap.has(prevTabKey)) {
setRowDataMap((prev) => {
const next = new Map(prev);
next.set(prevTabKey, latestRowsInPrevTab);
return next;
});
}
activeTabRef.current = tabKey;
setActiveTab(tabKey);
if (!hasSearched || !lastSearchedValues || !searchParamRef.current) return;
@@ -941,22 +1003,22 @@ const SewagePipeMaintainInp = () => {
tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable,
) || isAdminInfo;
const columns = (tab.columns ?? []).map((col: ColumnConfig) =>
canEdit
? col
: {
...col,
editable: false as const,
styleName: col.styleName ?? "rg-cell-readonly",
},
);
const rawColumns = tab.columns ?? [];
const base = canEdit
? rawColumns
: rawColumns.map((col) => ({ ...col, editable: false as const }));
const columns = base
.map((col) => {
const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim();
return guide ? { ...col, tooltip: guide } : col;
});
map.set(tabKey, {
columns,
columnLayout: tab.columnLayout ?? [],
});
}
return map;
}, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo]);
}, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, columnGuidesByTab]);
const searchContextForToolbar = lastSearchedValues ?? searchValues;
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
@@ -1059,6 +1121,9 @@ const SewagePipeMaintainInp = () => {
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog}
isVerificationFailed={
isVerificationFailedByTab.get(activeTab) === true
}
/>
{showEnterableToolbarButtons && (
<>
@@ -1110,11 +1175,7 @@ const SewagePipeMaintainInp = () => {
return (
<TabsContent key={tabKey} value={tabKey}>
<DataGrid
key={
tabEnterableForGrid || isAdminInfo
? `${tabKey}-enterable`
: `${tabKey}-readonly`
}
key={`${tabKey}-${tabEnterableForGrid ? "enterable" : "readonly"}`}
columnInfo={
columnInfoByTab.get(tabKey) ??
toGridColumnInfo(info)
@@ -1157,6 +1218,17 @@ const SewagePipeMaintainInp = () => {
open={verificationHistoryOpen}
onOpenChange={setVerificationHistoryOpen}
queryContext={verificationQueryContext}
onExceptionResetSubmitted={async () => {
const values = lastSearchedValues ?? searchValues;
if (!values || !searchParamRef.current) return;
await loadActiveTabData(
activeTabRef.current,
searchParamRef.current,
values,
true,
false,
);
}}
/>
<ConfirmDialog isOpen={false} />
<SubmitDailog
@@ -1170,6 +1242,17 @@ const SewagePipeMaintainInp = () => {
open={verificationResultDialogOpen}
onOpenChange={setVerificationResultDialogOpen}
queryContext={verificationQueryContext}
onExceptionSubmitted={async () => {
const values = lastSearchedValues ?? searchValues;
if (!values || !searchParamRef.current) return;
await loadActiveTabData(
activeTabRef.current,
searchParamRef.current,
values,
true,
false,
);
}}
/>
</>
);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -49,6 +49,7 @@ import { doList } from "@/components/util/column/input/DoList";
import { SeweragePenetrationRateInpColumn } from "@/components/util/column/input/SeweragePenetrationRateInpColumn";
import { getFooterSummary } from "@/components/util/grid/footerSummary";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { isVerificationFailedFromCommonInfo } from "@/components/util/input/verification-failed-flag";
import { useAdminStore } from "@/store/admin-store";
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
import { useTabStore } from "@/store/tab-store";
@@ -131,6 +132,7 @@ const SeweragePenetrationRateInp = () => {
/** 예외제출 버튼으로 연 다이얼로그인지 (확인 시 executeSubmit 대신 SubmitExceptionFunc 경로) */
const [isExceptionSubmitFlow, setIsExceptionSubmitFlow] = useState(false);
const [statsCd, setStatsCd] = useState<string>('');
const [isVerificationFailed, setIsVerificationFailed] = useState(false);
/** `engColNm` → `columnGuide` (헤더 hover) */
const [columnGuidesByEngCol, setColumnGuidesByEngCol] = useState<Record<string, string>>({});
const [footerFromApi, setFooterFromApi] = useState<{ sewerBzmnWholPpltnCnt?: number; sewerBzmnDomplWholPpltnCnt?: number; sewerBzmnFrgnrWholPpltnCnt?: number; sewerBzmnArea?: number } | null>(null);
@@ -240,6 +242,7 @@ const SeweragePenetrationRateInp = () => {
setLastSearchedValues(null);
setFooterFromApi(null);
setVerificationSearchDefaults(null);
setIsVerificationFailed(false);
setSearchFormRemountKey((k) => k + 1);
};
@@ -275,6 +278,7 @@ const SeweragePenetrationRateInp = () => {
: null
);
setStatsCd(commonInfo.statsCd);
setIsVerificationFailed(isVerificationFailedFromCommonInfo(commonInfo));
if (statusInfo && statusInfo.laeSbmsnSttsCd) {
if (['2', '3'].includes(statusInfo.laeSbmsnSttsCd)) {
setIsEnterable(false);
@@ -288,7 +292,8 @@ const SeweragePenetrationRateInp = () => {
} else {
alert(response.message);
}
} catch {
} catch (error) {
console.log(error);
alert('데이터 조회 중 오류가 발생했습니다.');
setRowData([]);
setStatusInfo(null);
@@ -778,6 +783,7 @@ const SeweragePenetrationRateInp = () => {
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
<VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog}
isVerificationFailed={isVerificationFailed}
/>
{showEnterableToolbarButtons && (
<>
@@ -790,11 +796,13 @@ const SeweragePenetrationRateInp = () => {
</div>
</div>
)}
{isLoading && !globalProgressVisible && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
{isLoading && !globalProgressVisible &&
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
</div>}
}
<DataGrid
key={(isEnterable || isAdminInfo) ? 'enterable' : 'readonly'}
columnInfo={columnInfo}
@@ -859,6 +867,11 @@ const SeweragePenetrationRateInp = () => {
}
: null
}
onExceptionResetSubmitted={async () => {
const values = lastSearchedValues ?? searchValues;
if (!values) return;
await handleSearch(values);
}}
/>
<VerificationResultDialog
open={verificationResultDialogOpen}
File diff suppressed because it is too large Load Diff
@@ -18,8 +18,6 @@ const StatisticsReviewHistory = () => {
{ 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 },
@@ -29,8 +27,6 @@ const StatisticsReviewHistory = () => {
"statsYr",
"ctpvNm",
"sggNm",
"sewerBzmnCd",
"statsCd",
"menuNm",
"chckerOpinionSm",
"wrterOpinionSm",
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,112 @@
import { Button } from "@common/components";
import DataGrid from "@/components/module/DataGrid";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import {
AdmdstRenewalReducedColumnConfig,
AdmdstRenewalAddedColumnConfig,
} from "@/components/util/column/management/AdmdstRenewalColumn";
import { UpperCodeSelectDialog } from "@/components/statistics/management/UpperCodeSelectDialog";
import type { useAdmdstRenewalTab } from "@/components/statistics/management/hooks/useAdmdstRenewalTab";
import { asGridRowData, asGridRowSetter, fromGridRow } from "@/components/statistics/management/lib/gridCast";
import type { AdmdstRenewalAddedItem } from "@/components/statistics/management/types/population-tab-types";
const reducedColumnInfo: ColumnInfo = {
columns: AdmdstRenewalReducedColumnConfig.columns ?? [],
columnLayout: AdmdstRenewalReducedColumnConfig.columnLayout ?? [],
};
const addedColumnInfo: ColumnInfo = {
columns: AdmdstRenewalAddedColumnConfig.columns ?? [],
columnLayout: AdmdstRenewalAddedColumnConfig.columnLayout ?? [],
};
type Props = {
tab: ReturnType<typeof useAdmdstRenewalTab>;
statsYr: string;
};
const AdmdstRenewalTab = ({ tab, statsYr }: Props) => {
return (
<div className="flex gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center mb-[1.1rem]">
<span className="text-[1.8rem] font-semibold text-gray-700 before:content-['●'] before:mr-1 before:text-blue-500">
감축된 행정구역(범위:동읍면) 자료
</span>
</div>
<div className="h-[3.4rem]" />
<DataGrid
columnInfo={reducedColumnInfo}
rowStartNum={AdmdstRenewalReducedColumnConfig.rowStartNum}
excelExportFileSuffix="감축된행정구역"
rowData={asGridRowData(tab.reducedRowData)}
setRowData={asGridRowSetter(tab.setReducedRowData)}
isExcelImport={false}
isExcelExport={false}
showHeaderCheckbox={false}
fixedColumns={AdmdstRenewalReducedColumnConfig.fixedColumns}
enableSort={false}
/>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center mb-[0.4rem]">
<span className="text-[1.8rem] font-semibold text-gray-700 before:content-['●'] before:mr-1 before:text-blue-500">
추가된 행정구역 자료
</span>
</div>
<div className="flex justify-end pr-[1rem] mb-[0.4rem] gap-4">
<Button
type="button"
color="red"
disabled={tab.isLoading}
onClick={() => void tab.handleRefresh()}
className="mt-[0.5rem] px-[4rem] py-[0.8rem] text-[1.4rem]"
>
갱신
</Button>
<Button
type="button"
color="primary"
disabled={tab.isLoading || (tab.reducedRowData.length === 0 && tab.addedRowData.length === 0)}
onClick={() => void tab.handleFinal(statsYr)}
className="mt-[0.5rem] px-[4rem] py-[0.8rem] text-[1.4rem]"
>
반영
</Button>
</div>
<DataGrid
columnInfo={addedColumnInfo}
rowStartNum={AdmdstRenewalAddedColumnConfig.rowStartNum}
excelExportFileSuffix="추가된행정구역"
rowData={asGridRowData(tab.addedRowData)}
setRowData={asGridRowSetter(tab.setAddedRowData)}
isExcelImport={false}
isExcelExport={false}
showHeaderCheckbox={false}
fixedColumns={AdmdstRenewalAddedColumnConfig.fixedColumns}
enableSort={false}
onCellDblClick={({ row, fieldName }) => {
if (fieldName !== "lawCd") return; // lawCd 셀만!
void tab.handleRowClick(fromGridRow<AdmdstRenewalAddedItem>(row), statsYr);
}}
/>
</div>
<UpperCodeSelectDialog
open={tab.dialogOpen}
onOpenChange={tab.setDialogOpen}
candidates={tab.candidates}
codeField="lawCd"
nameField="lawNm"
extraFields={[
{ field: "rgnCd", header: "지역코드" },
{ field: "lv", header: "레벨" },
]}
onSelect={tab.handleCodeSelect}
/>
</div>
);
};
export default AdmdstRenewalTab;
@@ -0,0 +1,136 @@
import { Button } from "@common/components";
import type { RealGridReact } from "realgrid-react";
import DataGrid from "@/components/module/DataGrid";
import type { ColumnInfo, GridViewWithEditCommit } from "@/components/type/DataGrid.type";
import {
DomesticPopulationLeftColumnConfig,
DomesticPopulationRightColumnConfig,
} from "@/components/util/column/management/DomesticPopulationColumn";
import { useRef } from "react";
import { UpperCodeSelectDialog } from "@/components/statistics/management/UpperCodeSelectDialog";
import type { usePopulationMappingTab } from "@/components/statistics/management/hooks/usePopulationMappingTab";
import { asGridRowData, asGridRowSetter, fromGridRow } from "@/components/statistics/management/lib/gridCast";
import type { DomesticPopulationItem } from "@/entities/statistics/model/ppltn/statistics-domestic-population-interface";
const leftColumnInfo: ColumnInfo = {
columns: DomesticPopulationLeftColumnConfig.columns ?? [],
columnLayout: DomesticPopulationLeftColumnConfig.columnLayout ?? [],
};
const rightColumnInfo: ColumnInfo = {
columns: DomesticPopulationRightColumnConfig.columns ?? [],
columnLayout: DomesticPopulationRightColumnConfig.columnLayout ?? [],
};
type DomesticTab = ReturnType<typeof usePopulationMappingTab<DomesticPopulationItem>>;
type Props = {
tab: DomesticTab;
statsYr: string;
onFileButtonClick: () => void;
};
const DomesticPopulationTab = ({ tab, statsYr, onFileButtonClick }: Props) => {
const rightGridRef = useRef<RealGridReact | null>(null);
return (
<div className="flex gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center mb-[0.4rem]">
<span className="text-[1.8rem] font-semibold text-gray-700 before:content-['●'] before:mr-1 before:text-blue-500">
엑셀 업로드 자료 (내국인 전체 리스트)
</span>
</div>
<div className="flex justify-end pr-2 mb-[0.4rem]">
<Button
type="button"
onClick={onFileButtonClick}
className="mt-[0.5rem] px-6 py-[0.8rem] text-[1.4rem] bg-gradient-green hover:bg-gradient-green-hover"
>
<i className="diveicon di-file-excel" />
엑셀 업로드
</Button>
</div>
<DataGrid
columnInfo={leftColumnInfo}
rowStartNum={DomesticPopulationLeftColumnConfig.rowStartNum}
excelExportFileSuffix="내국인전체리스트"
rowData={asGridRowData(tab.leftRowData)}
setRowData={asGridRowSetter(tab.setLeftRowData)}
isExcelImport={false}
isExcelExport={false}
showHeaderCheckbox={false}
fixedColumns={DomesticPopulationLeftColumnConfig.fixedColumns}
enableSort={false}
/>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center mb-[0.4rem]">
<span className="text-[1.8rem] font-semibold text-gray-700 before:content-['●'] before:mr-1 before:text-blue-500">
미매핑 리스트 (작년 대비)
</span>
</div>
<div className="flex justify-end pr-[1rem] mb-[0.4rem]">
<Button
type="button"
color="primary"
disabled={tab.isLoading || tab.rightRowData.length === 0}
onClick={() => void tab.handleFinal(statsYr)}
className="mt-[0.5rem] px-[4rem] py-[0.8rem] text-[1.4rem]"
>
반영
</Button>
</div>
<DataGrid
columnInfo={rightColumnInfo}
rowStartNum={DomesticPopulationRightColumnConfig.rowStartNum}
excelExportFileSuffix="미매핑리스트"
rowData={asGridRowData(tab.rightRowData)}
setRowData={asGridRowSetter(tab.setRightRowData)}
isExcelImport={false}
isExcelExport={false}
showHeaderCheckbox={false}
fixedColumns={DomesticPopulationRightColumnConfig.fixedColumns}
enableSort={false}
getGridRef={rightGridRef} // ← 추가
onDataCellClick={({ row, fieldName }) => {
if (fieldName === "mpinCd") return;
if (fieldName === "admdstCd") {
const gridView = rightGridRef.current?.gridView as
| GridViewWithEditCommit
| undefined;
gridView?.commit?.();
setTimeout(() => {
tab.setRightRowData((prev) =>
prev.map((r) =>
r.admdstCd === row.admdstCd
? { ...r, mpinCd: String(row.admdstCd ?? "") }
: r,
),
);
}, 0);
return;
}
}}
onCellDblClick={({ row, fieldName }) => {
if (fieldName !== "mpinCd") return;
void tab.handleRightRowClick(fromGridRow<DomesticPopulationItem>(row), statsYr);
}}
/>
</div>
<UpperCodeSelectDialog
open={tab.dialogOpen}
onOpenChange={tab.setDialogOpen}
candidates={tab.candidates}
codeField="admdstCd"
nameField="admdstNm"
onSelect={tab.handleCodeSelect}
/>
</div>
);
};
export default DomesticPopulationTab;

Some files were not shown because too many files have changed in this diff Show More