Compare commits
91
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2396f093b | ||
|
|
bdf65dcfd1 | ||
|
|
2aefbe17ee | ||
|
|
4482b92ccd | ||
|
|
d3c39caefb | ||
|
|
24c5dce24d | ||
|
|
d4d9eaf773 | ||
|
|
68a800cdfc | ||
|
|
ff45d1a875 | ||
|
|
49000c6140 | ||
|
|
24bba484ca | ||
|
|
c8d0d85937 | ||
|
|
ad1e517472 | ||
|
|
17ed5eb46b | ||
|
|
1a2faf02da | ||
|
|
d6522338aa | ||
|
|
df154a45b6 | ||
|
|
d438755616 | ||
|
|
4ee8d5a099 | ||
|
|
538f656640 | ||
|
|
8df79fd624 | ||
|
|
730be25948 | ||
|
|
9c38165afb | ||
|
|
9242e1fb47 | ||
|
|
271b5d445d | ||
|
|
2457d14ae9 | ||
|
|
9739609de4 | ||
|
|
c799c199c5 | ||
|
|
5b3aaa463d | ||
|
|
947dba5934 | ||
|
|
4816a512bf | ||
|
|
4fad163b14 | ||
|
|
da2191f365 | ||
|
|
55ab2b8562 | ||
|
|
526392c370 | ||
|
|
6020c80a7a | ||
|
|
32b778f961 | ||
|
|
3946fda013 | ||
|
|
db30b45267 | ||
|
|
6cc2a02df4 | ||
|
|
0f107ff29d | ||
|
|
5e69a9b6f0 | ||
|
|
ec9311c76b | ||
|
|
4c85a7d249 | ||
|
|
09d4c2d26e | ||
|
|
b8dac5dc72 | ||
|
|
73cafc9786 | ||
|
|
c276ae0c5e | ||
|
|
b4ed061a53 | ||
|
|
ddaaa8fc92 | ||
|
|
3a253df58a | ||
|
|
21debe2f77 | ||
|
|
0e4f662dfd | ||
|
|
4a797808c1 | ||
|
|
3a81b822f2 | ||
|
|
f5a7efb324 | ||
|
|
abc3ca184d | ||
|
|
267c022d58 | ||
|
|
0fc2844928 | ||
|
|
7f1fe42ace | ||
|
|
c83a13d680 | ||
|
|
3d7dcb6d0e | ||
|
|
a46f24d6d7 | ||
|
|
2e4f2428f0 | ||
|
|
706cebfeae | ||
|
|
ee7f5e3a07 | ||
|
|
3d0160f21f | ||
|
|
0aa0126caf | ||
|
|
5ecdd37187 | ||
|
|
20cc2ca248 | ||
|
|
5cfd7d65ec | ||
|
|
3b8307b674 | ||
|
|
2f800c960d | ||
|
|
2728b4058b | ||
|
|
b842905572 | ||
|
|
85fa9a8658 | ||
|
|
5ae56481c2 | ||
|
|
8079d7d6af | ||
|
|
7c8eef1715 | ||
|
|
f10b3179c2 | ||
|
|
2e0bb5aa84 | ||
|
|
7ae8d42731 | ||
|
|
a9e65af77b | ||
|
|
d21ecc975f | ||
|
|
452509281a | ||
|
|
2e3602eee6 | ||
|
|
7dfd535841 | ||
|
|
39d928b5b1 | ||
|
|
525695072a | ||
|
|
972a4ce789 | ||
|
|
879aadc068 |
@@ -0,0 +1,3 @@
|
||||
import { NtitlkSndngPage } from "@/pages/ntitlk-sndng";
|
||||
|
||||
export default NtitlkSndngPage;
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
DashboardMainResponseDto,
|
||||
DashboardMainSearchDto,
|
||||
DashboardService,
|
||||
InPrgrsDocPicResponseDto,
|
||||
} from "../model/dashboard-interface";
|
||||
|
||||
const BASE = ENDPOINT.AS_SERVICE.DASHBOARD;
|
||||
@@ -18,6 +19,14 @@ async function getMainDashboard(
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function getInPrgrsDocPicList(searchYear?: number): Promise<InPrgrsDocPicResponseDto[]> {
|
||||
const response = await client.get<InPrgrsDocPicResponseDto[]>(`${BASE}/in-progress-docs`, {
|
||||
params: { searchYear },
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const dashboardService = {
|
||||
getMainDashboard,
|
||||
getInPrgrsDocPicList,
|
||||
} satisfies DashboardService;
|
||||
|
||||
@@ -2,7 +2,10 @@ import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type { DashboardMainSearchDto } from "../model/dashboard-interface";
|
||||
import type {
|
||||
DashboardMainSearchDto,
|
||||
InPrgrsDocPicResponseDto,
|
||||
} from "../model/dashboard-interface";
|
||||
|
||||
import { dashboardService } from "./dashboard-service";
|
||||
|
||||
@@ -16,3 +19,14 @@ export function useGetMainDashboard(params: DashboardMainSearchDto = EMPTY_SEARC
|
||||
queryFn: () => dashboardService.getMainDashboard(params),
|
||||
});
|
||||
}
|
||||
|
||||
export function useGetInPrgrsDocPicList<TData = InPrgrsDocPicResponseDto[]>(
|
||||
searchYear?: number,
|
||||
options?: { select?: (data: InPrgrsDocPicResponseDto[]) => TData },
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: [...dashboardBaseKey, "getInPrgrsDocPicList", searchYear],
|
||||
queryFn: () => dashboardService.getInPrgrsDocPicList(searchYear),
|
||||
select: options?.select,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
export { dashboardService } from "./api/dashboard-service";
|
||||
export { dashboardBaseKey, useGetMainDashboard } from "./api/queries";
|
||||
export { dashboardBaseKey, useGetInPrgrsDocPicList, useGetMainDashboard } from "./api/queries";
|
||||
export type {
|
||||
BasinDocPrgrsResponseDto,
|
||||
DashboardMainResponseDto,
|
||||
DashboardMainSearchDto,
|
||||
DashboardService,
|
||||
DocPrgrsSttsResponseDto,
|
||||
InPrgrsDocPicResponseDto,
|
||||
NotiAsStatResponseDto,
|
||||
NotiAsUserResponseDto,
|
||||
} from "./model/dashboard-interface";
|
||||
|
||||
@@ -65,7 +65,22 @@ export interface DashboardMainResponseDto {
|
||||
notiStats: NotiAsStatResponseDto;
|
||||
}
|
||||
|
||||
/** 백엔드 InPrgrsDocPicResponseDto */
|
||||
export interface InPrgrsDocPicResponseDto {
|
||||
docNo: string;
|
||||
docNm?: string;
|
||||
docSeComCd: string;
|
||||
docSeNm?: string;
|
||||
aplcntNm?: string;
|
||||
aplcntMblTelno?: string;
|
||||
prgrsSttsComCd?: string;
|
||||
prgrsSttsNm?: string;
|
||||
regDt?: string;
|
||||
prcsDdln?: string;
|
||||
}
|
||||
|
||||
/** Controller public 메서드명·시그니처와 동일 */
|
||||
export interface DashboardService {
|
||||
getMainDashboard(searchDto: DashboardMainSearchDto): Promise<DashboardMainResponseDto>;
|
||||
getInPrgrsDocPicList(searchYear?: number): Promise<InPrgrsDocPicResponseDto[]>;
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ function AprvrPickerDialog({ open, onOpenChange, atrzSeComCd, onSelect }: AprvrP
|
||||
배치된 검토인력이 없습니다.
|
||||
</p>
|
||||
) : (
|
||||
<Table className="w-full">
|
||||
<Table caption="결재자 선택 목록으로 성명, 부서, 직위, 직급, 선택 정보 제공" className="w-full">
|
||||
<colgroup>
|
||||
<col width="25%" />
|
||||
<col width="25%" />
|
||||
|
||||
@@ -162,7 +162,7 @@ function ElaprAplnPickerGrid({
|
||||
</p>
|
||||
) : (
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<Table className="w-full">
|
||||
<Table caption="결재선 선택 목록으로 결재순서, 결재구분, 결재자 정보 제공" className="w-full">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
|
||||
@@ -315,7 +315,7 @@ function ElaprApprovalDialog({
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<Table className="w-full">
|
||||
<Table caption="결재 진행 현황 목록으로 결재순서, 결재구분, 결재자, 전결자 지정, 결재상태, 관리 정보 제공" className="w-full">
|
||||
<colgroup>
|
||||
<col width="8%" />
|
||||
<col width="8%" />
|
||||
|
||||
@@ -66,6 +66,7 @@ function ElaprTmpltSelectTable({
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "결재서식 선택 목록",
|
||||
onRowClick: (row) => {
|
||||
setRowSelection({ [row.elaprTmpltId]: true });
|
||||
onSelectTmplt(row);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { PageResponse } from "@/shared/model";
|
||||
|
||||
import type {
|
||||
BusinessSendRequest,
|
||||
BusinessSendResponse,
|
||||
NtftalkService,
|
||||
NtftalkTemplate,
|
||||
NtftalkTemplateSearchDto,
|
||||
TemplateVariablesResponse,
|
||||
} from "../model/ntftalk-interface";
|
||||
|
||||
const { TEMP, ATA_SEND } = ENDPOINT.NTFTALK_SERVICE;
|
||||
|
||||
async function findTemplatePage(
|
||||
params: NtftalkTemplateSearchDto,
|
||||
): Promise<PageResponse<NtftalkTemplate>> {
|
||||
const response = await client.get<PageResponse<NtftalkTemplate>>(TEMP, { params });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function findTemplateVariables(templateCode: string): Promise<TemplateVariablesResponse> {
|
||||
const response = await client.get<TemplateVariablesResponse>(
|
||||
`${ATA_SEND}/template-variables/${templateCode}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function sendBusiness(body: BusinessSendRequest): Promise<BusinessSendResponse> {
|
||||
const response = await client.post<BusinessSendResponse>(`${ATA_SEND}/business`, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const ntftalkService = {
|
||||
findTemplatePage,
|
||||
findTemplateVariables,
|
||||
sendBusiness,
|
||||
} satisfies NtftalkService;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useMutationWrapper } from "@/shared/lib";
|
||||
|
||||
import type { BusinessSendRequest, NtftalkTemplateSearchDto } from "../model/ntftalk-interface";
|
||||
|
||||
import { ntftalkService } from "./ntftalk-service";
|
||||
|
||||
/** 발송 화면에서 선택 가능한 템플릿: 검수 승인(APR)된 기본형(BA) */
|
||||
const SNDNG_TEMPLATE_SEARCH: NtftalkTemplateSearchDto = {
|
||||
sort: "createdAt,desc",
|
||||
senderKey: "",
|
||||
inspectionStatus: "APR",
|
||||
templateMessageType: "BA",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
export function useNtftalkTemplateList() {
|
||||
return useQuery({
|
||||
queryKey: [ENDPOINT.NTFTALK_SERVICE.TEMP, "findTemplatePage", SNDNG_TEMPLATE_SEARCH],
|
||||
queryFn: () => ntftalkService.findTemplatePage(SNDNG_TEMPLATE_SEARCH),
|
||||
});
|
||||
}
|
||||
|
||||
export function useNtftalkTemplateVariables(templateCode: string) {
|
||||
return useQuery({
|
||||
queryKey: [ENDPOINT.NTFTALK_SERVICE.ATA_SEND, "findTemplateVariables", templateCode],
|
||||
queryFn: () => ntftalkService.findTemplateVariables(templateCode),
|
||||
enabled: Boolean(templateCode),
|
||||
});
|
||||
}
|
||||
|
||||
export function useSendNtftalkBusiness() {
|
||||
return useMutationWrapper({
|
||||
mutationFn: (body: BusinessSendRequest) => ntftalkService.sendBusiness(body),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export { ntftalkService } from "./api/ntftalk-service";
|
||||
export {
|
||||
useNtftalkTemplateList,
|
||||
useNtftalkTemplateVariables,
|
||||
useSendNtftalkBusiness,
|
||||
} from "./api/queries";
|
||||
export type {
|
||||
BusinessSendRequest,
|
||||
BusinessSendResponse,
|
||||
NtftalkService,
|
||||
NtftalkTemplate,
|
||||
NtftalkTemplateSearchDto,
|
||||
TemplateVariablesResponse,
|
||||
} from "./model/ntftalk-interface";
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { PageableParams, PageResponse } from "@/shared/model";
|
||||
|
||||
/** 백엔드 알림톡 템플릿 응답 */
|
||||
export interface NtftalkTemplate {
|
||||
templateCode: string;
|
||||
templateName: string;
|
||||
templateContent: string;
|
||||
}
|
||||
|
||||
/** 템플릿 목록 검색 조건 + Pageable */
|
||||
export interface NtftalkTemplateSearchDto extends PageableParams {
|
||||
senderKey?: string;
|
||||
inspectionStatus?: string;
|
||||
templateMessageType?: string;
|
||||
}
|
||||
|
||||
export interface TemplateVariablesResponse {
|
||||
templateCode: string;
|
||||
variables: string[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface BusinessSendRequest {
|
||||
templateCode: string;
|
||||
callback?: string;
|
||||
fixedRecipientNum?: string;
|
||||
recipients: Record<string, string>[];
|
||||
}
|
||||
|
||||
export interface BusinessSendResponse {
|
||||
success: boolean;
|
||||
sentCount: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/** Controller public 메서드명·시그니처와 동일 */
|
||||
export interface NtftalkService {
|
||||
findTemplatePage(params: NtftalkTemplateSearchDto): Promise<PageResponse<NtftalkTemplate>>;
|
||||
findTemplateVariables(templateCode: string): Promise<TemplateVariablesResponse>;
|
||||
sendBusiness(body: BusinessSendRequest): Promise<BusinessSendResponse>;
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { PageResponse } from "@/shared/model";
|
||||
|
||||
import type {
|
||||
RvwMnpwMngPageSearchDto,
|
||||
RvwMnpwMngResponseDto,
|
||||
RvwMnpwMngSaveRequestDto,
|
||||
RvwMnpwMngSearchDto,
|
||||
RvwMnpwMngUpdateRequestDto,
|
||||
@@ -13,14 +15,18 @@ import { rvwMnpwMngService } from "./rvw-mnpw-service";
|
||||
|
||||
const rvwMnpwMngBaseKey = [ENDPOINT.AS_SERVICE.RVW_MNPW] as const;
|
||||
|
||||
export function useRvwMnpwMngPage(
|
||||
export function useRvwMnpwMngPage<TData = PageResponse<RvwMnpwMngResponseDto>>(
|
||||
params: RvwMnpwMngPageSearchDto,
|
||||
options?: { enabled?: boolean },
|
||||
options?: {
|
||||
enabled?: boolean;
|
||||
select?: (data: PageResponse<RvwMnpwMngResponseDto>) => TData;
|
||||
},
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: [...rvwMnpwMngBaseKey, "findPage", params],
|
||||
queryFn: () => rvwMnpwMngService.findPage(params),
|
||||
enabled: options?.enabled,
|
||||
select: options?.select,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface RvwMnpwMngResponseDto {
|
||||
ognzSn: number;
|
||||
userUuid: string;
|
||||
userUuidNm?: string;
|
||||
mblTelno?: string;
|
||||
ognzNm?: string;
|
||||
jbpsSeNm?: string;
|
||||
jbgdSeNm?: string;
|
||||
|
||||
@@ -73,7 +73,7 @@ function AprvrPickerDialog({
|
||||
배치된 검토인력이 없습니다.
|
||||
</p>
|
||||
) : (
|
||||
<Table className="w-full">
|
||||
<Table caption="검토인력 목록으로 성명, 부서, 직위, 직급, 선택 정보 제공" className="w-full">
|
||||
<colgroup>
|
||||
<col width="25%" />
|
||||
<col width="25%" />
|
||||
|
||||
@@ -185,7 +185,7 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
|
||||
</p>
|
||||
) : (
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<Table className="w-full">
|
||||
<Table caption="결재선 목록으로 결재순서, 결재구분, 결재자 정보 제공" className="w-full">
|
||||
<colgroup>
|
||||
<col width="5%" />
|
||||
<col width="10%" />
|
||||
|
||||
@@ -90,6 +90,7 @@ function ElaprTmpltList({ selectedTmpltId, onSelectTmplt }: ElaprTmpltListProps)
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "결재서식 목록으로 결재서식명 정보 제공",
|
||||
onRowClick: (row) => {
|
||||
setRowSelection({ [row.elaprTmpltId]: true });
|
||||
onSelectTmplt(row);
|
||||
|
||||
@@ -67,7 +67,12 @@ function CklstArtclList({ cklstSn }: CklstArtclListProps) {
|
||||
</div>
|
||||
),
|
||||
Empty: <NoData />,
|
||||
Success: () => <DataTable table={table} />,
|
||||
Success: () => (
|
||||
<DataTable
|
||||
table={table}
|
||||
caption="체크리스트 항목 목록: 항목명, 검토내용, 검토결과 정보 제공"
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
const handleFormSubmit = async (values: CklstArtclFormValues) => {
|
||||
|
||||
@@ -83,6 +83,7 @@ function CklstMngList({ selectedCklstSn, onSelectCklst }: CklstMngListProps) {
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "체크리스트 목록",
|
||||
onRowClick: (row) => {
|
||||
setRowSelection({ [String(row.cklstSn)]: true });
|
||||
onSelectCklst(row);
|
||||
|
||||
@@ -51,7 +51,7 @@ function CklstPreviewDialog({
|
||||
{displayRows.length === 0 ? (
|
||||
<p className="py-6 text-center text-muted-foreground">표시할 항목이 없습니다.</p>
|
||||
) : (
|
||||
<Table>
|
||||
<Table caption={`체크리스트 ${cklstNm} 미리보기`}>
|
||||
<colgroup>
|
||||
<col width="25%" />
|
||||
<col width="50%" />
|
||||
|
||||
@@ -277,7 +277,7 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) {
|
||||
등록된 항목이 없습니다. [추가]로 1단계 항목을 등록하세요.
|
||||
</p>
|
||||
) : (
|
||||
<Table className="w-full">
|
||||
<Table caption="체크리스트 항목 목록" className="w-full">
|
||||
<colgroup>
|
||||
<col width="16%" />
|
||||
<col width="36%" />
|
||||
|
||||
@@ -103,7 +103,7 @@ function AcntgCodeTabPanel() {
|
||||
</p>
|
||||
) : (
|
||||
<div className="max-h-[60rem] overflow-y-auto px-4 pb-4 pt-1">
|
||||
<Table className="w-full">
|
||||
<Table caption="회계구분 코드 목록" className="w-full">
|
||||
<colgroup>
|
||||
<col width="14%" />
|
||||
<col width="16%" />
|
||||
|
||||
@@ -103,7 +103,7 @@ function CnstpCodeTabPanel() {
|
||||
</p>
|
||||
) : (
|
||||
<div className="max-h-[60rem] overflow-y-auto px-4 pb-4 pt-1">
|
||||
<Table className="w-full">
|
||||
<Table caption="공종구분 코드 목록" className="w-full">
|
||||
<colgroup>
|
||||
<col width="14%" />
|
||||
<col width="16%" />
|
||||
|
||||
@@ -103,7 +103,7 @@ function FnrsSharCodeTabPanel() {
|
||||
</p>
|
||||
) : (
|
||||
<div className="max-h-[60rem] overflow-y-auto px-4 pb-4 pt-1">
|
||||
<Table className="w-full">
|
||||
<Table caption="재원분담 코드 목록" className="w-full">
|
||||
<colgroup>
|
||||
<col width="14%" />
|
||||
<col width="16%" />
|
||||
|
||||
@@ -78,6 +78,15 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.svgContainer :global(svg) :global(.basin:focus-visible) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.svgContainer :global(svg) :global(.basin:focus-visible) :global(path) {
|
||||
stroke: #111827;
|
||||
stroke-width: 2.5;
|
||||
}
|
||||
|
||||
.svgContainer :global(svg) :global(.basin) :global(path),
|
||||
.svgContainer :global(svg) :global(.basin) :global(polygon),
|
||||
.svgContainer :global(svg) :global(.basin) :global(polyline) {
|
||||
@@ -146,7 +155,7 @@
|
||||
}
|
||||
|
||||
.basinLabel.active {
|
||||
background: #006bf8;
|
||||
background: #0056b3;
|
||||
color: #fff;
|
||||
border-color: #96c3ff;
|
||||
box-shadow: 0 4px 12px rgba(0, 107, 248, 0.3);
|
||||
|
||||
@@ -265,20 +265,44 @@ export function useNationalAsMap(basinDocPrgrsList: BasinDocPrgrsResponseDto[] |
|
||||
};
|
||||
|
||||
groups.forEach((group) => {
|
||||
addListener(group, "mouseenter", () => {
|
||||
group.setAttribute("role", "button");
|
||||
group.setAttribute("tabindex", "0");
|
||||
group.setAttribute(
|
||||
"aria-label",
|
||||
`${resolveBasinName(group)} 행정지원 현황 보기`,
|
||||
);
|
||||
|
||||
const activateGroup = () => {
|
||||
clearActive?.();
|
||||
group.classList.add("active");
|
||||
revealGroup(group);
|
||||
};
|
||||
const clearGroupIfInactive = () => {
|
||||
if (!group.matches(":hover") && document.activeElement !== group) {
|
||||
clearActive?.();
|
||||
}
|
||||
};
|
||||
|
||||
addListener(group, "mouseenter", () => {
|
||||
activateGroup();
|
||||
});
|
||||
addListener(group, "mousemove", () => {
|
||||
if (group.classList.contains("active")) revealGroup(group);
|
||||
});
|
||||
addListener(group, "mouseleave", () => clearActive?.());
|
||||
addListener(group, "mouseleave", clearGroupIfInactive);
|
||||
addListener(group, "focus", activateGroup);
|
||||
addListener(group, "blur", clearGroupIfInactive);
|
||||
addListener(group, "click", (event) => {
|
||||
event.stopPropagation();
|
||||
clearActive?.();
|
||||
group.classList.add("active");
|
||||
revealGroup(group);
|
||||
activateGroup();
|
||||
});
|
||||
addListener(group, "keydown", (event) => {
|
||||
const keyboardEvent = event as KeyboardEvent;
|
||||
if (keyboardEvent.key !== "Enter" && keyboardEvent.key !== " ") {
|
||||
return;
|
||||
}
|
||||
keyboardEvent.preventDefault();
|
||||
activateGroup();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -40,18 +40,18 @@ export function DashboardNotiList({
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="card card-border m-0 bg-background">
|
||||
<section className="card card-border m-0 bg-[#FAFBFC]">
|
||||
<div className="card-header flex items-center justify-between gap-4">
|
||||
<h3 className="text-2xl font-semibold">
|
||||
알림이력
|
||||
{dataUpdatedAt > 0 ? (
|
||||
<span className="pl-2 text-lg font-medium text-muted-foreground">
|
||||
<span className="pl-2 text-lg font-medium text-[#4A4A4A]">
|
||||
(업데이트 일시: {dayjs(dataUpdatedAt).format("YYYY-MM-DD HH:mm")})
|
||||
</span>
|
||||
) : null}
|
||||
</h3>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button type="button" size="sm" color="white" onClick={() => router.push("/nti-hstry")}>
|
||||
<Button type="button" size="sm" color="white" className="!text-[#0056B3]" onClick={() => router.push("/nti-hstry")}>
|
||||
전체보기
|
||||
</Button>
|
||||
</div>
|
||||
@@ -60,7 +60,7 @@ export function DashboardNotiList({
|
||||
<div className="flex flex-col gap-4 p-2">
|
||||
<div className="flex flex-wrap gap-2 pt-2">
|
||||
{NOTI_STAT_CHIPS.map((chip) => (
|
||||
<Badge key={chip.field} color={chip.color}>
|
||||
<Badge key={chip.field} color={chip.color} className={chip.className}>
|
||||
{chip.label} {isPending ? "-" : (notiStats?.[chip.field] ?? 0)}건
|
||||
</Badge>
|
||||
))}
|
||||
@@ -75,7 +75,7 @@ export function DashboardNotiList({
|
||||
) : null}
|
||||
|
||||
{isError ? (
|
||||
<p className="flex min-h-[12rem] items-center justify-center text-muted-foreground">
|
||||
<p className="flex min-h-[12rem] items-center justify-center text-[#4A4A4A]">
|
||||
알림이력을 불러오지 못했습니다.
|
||||
</p>
|
||||
) : null}
|
||||
@@ -110,11 +110,11 @@ interface DashboardNotiRowProps {
|
||||
function DashboardNotiRow({ row, isActionPending, onAction }: DashboardNotiRowProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-4 border-b px-2 py-4 last:border-b-0">
|
||||
<span className="text-lg text-muted-foreground">
|
||||
<span className="text-lg text-[#4A4A4A]">
|
||||
{row.regDt ? dayjs(row.regDt).format("YYYY-MM-DD") : "-"}
|
||||
</span>
|
||||
<p className="min-w-0 line-clamp-2 text-xl sm:line-clamp-1">{row.notiTtl}</p>
|
||||
<Button type="button" size="sm" color="white" disabled={isActionPending} onClick={onAction}>
|
||||
<Button type="button" size="sm" color="white" className="!text-[#0056B3]" disabled={isActionPending} onClick={onAction}>
|
||||
처리하기
|
||||
</Button>
|
||||
</div>
|
||||
@@ -137,7 +137,7 @@ function resolveNotiUrl(row: NotiAsUserResponseDto) {
|
||||
}
|
||||
|
||||
const NOTI_STAT_CHIPS = [
|
||||
{ field: "todayCount", label: "오늘", color: "green" },
|
||||
{ field: "delayCount", label: "지연", color: "orange" },
|
||||
{ field: "pendingCount", label: "완료대기", color: "gray" },
|
||||
{ field: "todayCount", label: "오늘", color: "green", className: "!bg-[#188038]" },
|
||||
{ field: "delayCount", label: "지연", color: "orange", className: "!bg-[#C5221F]" },
|
||||
{ field: "pendingCount", label: "완료대기", color: "gray", className: "!bg-[#5F6368]" },
|
||||
] as const;
|
||||
|
||||
@@ -39,16 +39,17 @@ export function DashboardNoticeTable() {
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "공지사항 목록",
|
||||
variant: "line",
|
||||
onRowClick: handleRowClick,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<section className="card card-border m-0 bg-background">
|
||||
<section className="card card-border m-0 bg-[#FAFBFC]">
|
||||
<div className="card-header flex items-center justify-between">
|
||||
<h3 className="text-2xl font-semibold">공지사항</h3>
|
||||
<Button type="button" size="sm" color="white" onClick={handleMoreClick}>
|
||||
<Button type="button" size="sm" color="white" className="!text-[#0056B3]" onClick={handleMoreClick}>
|
||||
더보기
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@ export function DashboardPage() {
|
||||
const { data, isPending, isError, dataUpdatedAt } = useGetMainDashboard();
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex gap-4 px-[1.8rem] pb-6">
|
||||
<div className="flex min-h-full gap-4 bg-[#E9EDF2] px-[1.8rem] pb-6 pt-4 text-[#1E1E1E]">
|
||||
<div className="hidden flex-[0.5] sm:block">
|
||||
<NationalAsMap basinDocPrgrsList={data?.basinDocPrgrsList} />
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ export function DocPrgrsSttsCards({ items, isPending, isError }: DocPrgrsSttsCar
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="card card-border m-0 flex min-h-[14rem] items-center justify-center bg-background text-muted-foreground">
|
||||
<div className="card card-border m-0 flex min-h-[14rem] items-center justify-center bg-[#FAFBFC] text-[#4A4A4A]">
|
||||
진행상황을 불러오지 못했습니다.
|
||||
</div>
|
||||
);
|
||||
@@ -39,7 +39,7 @@ export function DocPrgrsSttsCards({ items, isPending, isError }: DocPrgrsSttsCar
|
||||
return (
|
||||
<div
|
||||
key={slot.docSeComCd}
|
||||
className="card card-border hover:bg-muted/40 m-0 block bg-background transition-colors"
|
||||
className="card card-border hover:bg-muted/40 m-0 block bg-[#FAFBFC] transition-colors"
|
||||
>
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-semibold">{slot.title}</h3>
|
||||
@@ -90,7 +90,7 @@ function DocPrgrsSttsMetric({
|
||||
>
|
||||
<Icon className="size-6" />
|
||||
</span>
|
||||
<span className="text-xl text-muted-foreground">{label}</span>
|
||||
<span className="text-xl text-[#4A4A4A]">{label}</span>
|
||||
<span className="text-3xl font-semibold tabular-nums">{count}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -107,21 +107,21 @@ const STAT_METRICS = [
|
||||
{
|
||||
field: "reqCnt",
|
||||
label: "신청",
|
||||
iconClassName: "bg-blue-500",
|
||||
iconClassName: "bg-[#4285F4]",
|
||||
bgColor: "bg-blue-100",
|
||||
Icon: FilePlus,
|
||||
},
|
||||
{
|
||||
field: "prgrsCnt",
|
||||
label: "진행",
|
||||
iconClassName: "bg-emerald-500",
|
||||
iconClassName: "bg-[#34A853]",
|
||||
bgColor: "bg-emerald-100",
|
||||
Icon: RefreshCw,
|
||||
},
|
||||
{
|
||||
field: "cmptnCnt",
|
||||
label: "완료",
|
||||
iconClassName: "bg-orange-500",
|
||||
iconClassName: "bg-[#EA4335]",
|
||||
bgColor: "bg-orange-100",
|
||||
Icon: CircleCheck,
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ export function NationalAsMap({ basinDocPrgrsList }: NationalAsMapProps) {
|
||||
const popupColumnSpan = Math.max(1, popupContent.items.length);
|
||||
|
||||
return (
|
||||
<section className="card card-border m-0 flex h-full min-h-[40rem] flex-col bg-background">
|
||||
<section className="card card-border m-0 flex h-full min-h-[40rem] flex-col bg-[#FAFBFC]">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-semibold">전국 행정지원 지도</h3>
|
||||
</div>
|
||||
|
||||
@@ -165,8 +165,11 @@ function DesignFormPage({ mode, initialData, createDefaults }: DesignFormPagePro
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSave)} className="mt-6 space-y-4">
|
||||
<SectionCollapsibleGroup>
|
||||
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
|
||||
<div className="sticky top-[6rem] z-30 flex items-center justify-between bg-container px-4 py-2">
|
||||
<SectionCollapsibleToggle />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<span className="text-red-600">*</span>은 필수입력 항목입니다.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{initialData?.detDto?.designCnstnDocNo && (
|
||||
|
||||
@@ -59,6 +59,7 @@ function DesignList({ searchParams, pagination, onPaginationChange }: DesignList
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "설계검토 목록",
|
||||
viewPagination: true,
|
||||
onRowClick: handleRowClick,
|
||||
},
|
||||
|
||||
@@ -36,7 +36,7 @@ function BasicInfoFormSection() {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<Table variant="secondary">
|
||||
<Table caption="사업 기본 정보 입력" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
@@ -45,7 +45,7 @@ function BasicInfoFormSection() {
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]" data-required>
|
||||
<TableHead scope="row" className="w-[20rem]" data-required>
|
||||
사업명
|
||||
</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
@@ -73,7 +73,7 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>사업기간</TableHead>
|
||||
<TableHead scope="row" data-required>사업기간</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<FieldGroup className="flex-row items-center gap-2">
|
||||
<Controller
|
||||
@@ -117,7 +117,7 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>사업내용</TableHead>
|
||||
<TableHead scope="row" data-required>사업내용</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<Controller
|
||||
name="designCnstnMng.bizCn"
|
||||
@@ -137,9 +137,9 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>구분</TableHead>
|
||||
<TableHead scope="row" data-required>구분</TableHead>
|
||||
<TableCell>{taskSeComCdLabel}</TableCell>
|
||||
<TableHead>신청 지자체</TableHead>
|
||||
<TableHead scope="row">신청 지자체</TableHead>
|
||||
<TableCell>{localGovLabel}</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
@@ -11,7 +11,7 @@ interface BasicInfoSectionProps {
|
||||
function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<Table variant="secondary">
|
||||
<Table caption="사업 기본 정보" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
|
||||
@@ -13,7 +13,7 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="사업규모">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<Table caption="사업규모 목록: 구분, 기본계획, 예산반영, 금회설계, 증감 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[30rem]">구분</TableHead>
|
||||
@@ -36,7 +36,7 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) {
|
||||
|
||||
return (
|
||||
<TableRow key={node.cnstpSeCd}>
|
||||
<TableHead className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||
<TableHead scope="row" className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||
{node.cnstpSeCdNm}
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
|
||||
@@ -11,21 +11,21 @@ interface ContactInfoSectionProps {
|
||||
function ContactInfoSection({ det }: ContactInfoSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="담당자 정보">
|
||||
<Table variant="secondary">
|
||||
<Table caption="사업 담당자 정보" variant="secondary">
|
||||
<TableHeader>
|
||||
{det.aplcntNm && (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2}>신청자</TableHead>
|
||||
<TableHead>이름</TableHead>
|
||||
<TableHead scope="row" rowSpan={2}>신청자</TableHead>
|
||||
<TableHead scope="row">이름</TableHead>
|
||||
<TableCell>{det.aplcntNm || "-"}</TableCell>
|
||||
<TableHead>연락처</TableHead>
|
||||
<TableHead scope="row">연락처</TableHead>
|
||||
<TableCell>{det.aplcntMblTelno || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>소속</TableHead>
|
||||
<TableHead scope="row">소속</TableHead>
|
||||
<TableCell>{det.aplcntOgdpNm || "-"}</TableCell>
|
||||
<TableHead>이메일</TableHead>
|
||||
<TableHead scope="row">이메일</TableHead>
|
||||
<TableCell>{det.aplcntEmlAddr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
@@ -33,16 +33,16 @@ function ContactInfoSection({ det }: ContactInfoSectionProps) {
|
||||
{det.dsgnrNm && (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2}>설계자</TableHead>
|
||||
<TableHead>이름</TableHead>
|
||||
<TableHead scope="row" rowSpan={2}>설계자</TableHead>
|
||||
<TableHead scope="row">이름</TableHead>
|
||||
<TableCell>{det.dsgnrNm || "-"}</TableCell>
|
||||
<TableHead>연락처</TableHead>
|
||||
<TableHead scope="row">연락처</TableHead>
|
||||
<TableCell>{det.dsgnrTelno || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>소속</TableHead>
|
||||
<TableHead scope="row">소속</TableHead>
|
||||
<TableCell>{det.dsgnrOgdpNm || "-"}</TableCell>
|
||||
<TableHead>이메일</TableHead>
|
||||
<TableHead scope="row">이메일</TableHead>
|
||||
<TableCell>{det.dsgnrEmlAddr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
|
||||
@@ -180,10 +180,10 @@ const TechReviewOpinionSection = forwardRef<
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||
<Table variant="secondary">
|
||||
<Table caption="검토의견 입력" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[12rem]">의견내용</TableHead>
|
||||
<TableHead scope="row" className="w-[12rem]">의견내용</TableHead>
|
||||
<TableCell>
|
||||
<Textarea
|
||||
value={formData.rvwRsltCn}
|
||||
@@ -197,7 +197,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>검토결과</TableHead>
|
||||
<TableHead scope="row">검토결과</TableHead>
|
||||
<TableCell>
|
||||
<FieldSet>
|
||||
<RadioGroup
|
||||
@@ -234,7 +234,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>첨부파일</TableHead>
|
||||
<TableHead scope="row">첨부파일</TableHead>
|
||||
<TableCell>
|
||||
{uploadedFile ? (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -289,7 +289,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<Table caption="검토의견 목록: 의견자, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="55%" />
|
||||
|
||||
@@ -100,6 +100,7 @@ function EsntlAtchFileList({
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "필수 첨부파일 관리 목록",
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, FnrsCnsltnViewBu
|
||||
},
|
||||
ROLE_USER04: {
|
||||
ST: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.envoSbmsn, FNRS_CNSLTN_VIEW_BUTTON_ACTION.rjct],
|
||||
EP: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.sncnAprv, FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
|
||||
EP: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.sncnAprv],
|
||||
},
|
||||
ROLE_USER03: {
|
||||
LE: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply, FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw],
|
||||
|
||||
@@ -53,7 +53,7 @@ function ChecklistTaskPanel({
|
||||
return (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<Table caption="체크리스트 검토 입력: 검토항목, 주요 검토내용, 검토결과 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]" rowSpan={2}>
|
||||
|
||||
@@ -115,6 +115,7 @@ function FileVrfcSplmntDialog({
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "파일 검증 보완요청 목록",
|
||||
viewPagination: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -42,7 +42,7 @@ function PwrkCnsltnDocPjtcoAjmtTable({
|
||||
}
|
||||
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<Table caption="사업비 조정 내역: 구분, 검증결과, 검증금액 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="30%" />
|
||||
<col width="50%" />
|
||||
|
||||
@@ -157,7 +157,7 @@ function CnsltnListTable({
|
||||
const isSomeSelected = selectedCount > 0 && !isAllSelected;
|
||||
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<Table caption="자문 검토결과 목록: 구분, 검토의견 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="4%" />
|
||||
<col width="4%" />
|
||||
|
||||
@@ -248,8 +248,11 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSave)} className="mt-4">
|
||||
<SectionCollapsibleGroup>
|
||||
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
|
||||
<div className="sticky top-[6rem] z-30 flex items-center justify-between bg-container px-4 py-2">
|
||||
<SectionCollapsibleToggle />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<span className="text-red-600">*</span>은 필수입력 항목입니다.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{initialData?.detDto?.fnrsCnsltnDocNo && (
|
||||
|
||||
@@ -59,6 +59,7 @@ function FnrsList({ searchParams, pagination, onPaginationChange }: FnrsListProp
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "재원협의 목록",
|
||||
viewPagination: true,
|
||||
onRowClick: handleRowClick,
|
||||
},
|
||||
|
||||
@@ -49,7 +49,7 @@ function BasicInfoFormSection() {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<Table variant="secondary">
|
||||
<Table caption="재원협의 기본 정보 입력" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
@@ -58,7 +58,7 @@ function BasicInfoFormSection() {
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]" data-required>
|
||||
<TableHead scope="row" className="w-[20rem]" data-required>
|
||||
사업명
|
||||
</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
@@ -86,7 +86,7 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>사업기간</TableHead>
|
||||
<TableHead scope="row" data-required>사업기간</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<FieldGroup className="flex-row items-center gap-2">
|
||||
<Controller
|
||||
@@ -130,7 +130,7 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>사업내용</TableHead>
|
||||
<TableHead scope="row" data-required>사업내용</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<Controller
|
||||
name="fnrsCnsltnMng.bizCn"
|
||||
@@ -150,15 +150,15 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>구분</TableHead>
|
||||
<TableHead scope="row" data-required>구분</TableHead>
|
||||
<TableCell>{taskSeComCdLabel}</TableCell>
|
||||
<TableHead>신청 지자체</TableHead>
|
||||
<TableHead scope="row">신청 지자체</TableHead>
|
||||
<TableCell>{localGovLabel}</TableCell>
|
||||
</TableRow>
|
||||
{taskSeComCd.includes("RE") && (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead>시설종류</TableHead>
|
||||
<TableHead scope="row">시설종류</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<Controller
|
||||
name="fnrsCnsltnMng.rtlztnFcltSeComCd"
|
||||
@@ -198,7 +198,7 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>재이용용도</TableHead>
|
||||
<TableHead scope="row">재이용용도</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<Controller
|
||||
name="fnrsCnsltnMng.rtlztnUsgSeComCd"
|
||||
|
||||
@@ -11,7 +11,7 @@ interface BasicInfoSectionProps {
|
||||
function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<Table variant="secondary">
|
||||
<Table caption="재원협의 기본 정보" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
@@ -20,31 +20,31 @@ function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>사업명</TableHead>
|
||||
<TableHead scope="row">사업명</TableHead>
|
||||
<TableCell>{det.bizNm || "-"}</TableCell>
|
||||
<TableHead>사업기간</TableHead>
|
||||
<TableHead scope="row">사업기간</TableHead>
|
||||
<TableCell>{(det.bizBgngYr || "-") + " ~ " + (det.bizEndYr || "-")}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사업내용</TableHead>
|
||||
<TableHead scope="row">사업내용</TableHead>
|
||||
<TableCell colSpan={3}>{det.bizCn || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>환경청구분</TableHead>
|
||||
<TableHead scope="row">환경청구분</TableHead>
|
||||
<TableCell>{det.envoSeCdNm || "-"}</TableCell>
|
||||
<TableHead>구분</TableHead>
|
||||
<TableHead scope="row">구분</TableHead>
|
||||
<TableCell>{det.taskSeCdNm || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>신청지자체</TableHead>
|
||||
<TableHead scope="row">신청지자체</TableHead>
|
||||
<TableCell>
|
||||
{det.sido || "-"} {det.gugun || "-"}
|
||||
</TableCell>
|
||||
<TableHead>확정예산사업번호</TableHead>
|
||||
<TableHead scope="row">확정예산사업번호</TableHead>
|
||||
<TableCell>{det.cfmtnBgtBizNo || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>예산신청연도</TableHead>
|
||||
<TableHead scope="row">예산신청연도</TableHead>
|
||||
<TableCell colSpan={3}>{det.bgtAplyYr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
@@ -20,7 +20,7 @@ function BusinessCostSection({ bizCstList }: BusinessCostSectionProps) {
|
||||
<SectionCollapsible title="총사업비">
|
||||
<div className="overflow-x-auto">
|
||||
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<Table caption="총사업비 내역: 구분, 예산반영, 표준사업비, 금회설계, 증감 정보 제공" variant="secondary" className="table-fixed">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[30rem]" rowSpan={2}>
|
||||
@@ -51,9 +51,9 @@ function BusinessCostSection({ bizCstList }: BusinessCostSectionProps) {
|
||||
return (
|
||||
<TableRow key={node.acntgSeCd}>
|
||||
{node.treeLevel <= 1 ? (
|
||||
<TableHead className="!px-[0.8rem] text-left">{node.acntgSeCdNm}</TableHead>
|
||||
<TableHead scope="row" className="!px-[0.8rem] text-left">{node.acntgSeCdNm}</TableHead>
|
||||
) : (
|
||||
<TableHead className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||
<TableHead scope="row" className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||
{showSubIcon && <span className="mr-1 text-gray-400">└</span>}
|
||||
{node.acntgSeCdNm}
|
||||
</TableHead>
|
||||
|
||||
@@ -13,7 +13,7 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="사업규모">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<Table caption="사업규모 내역: 구분, 기본계획, 예산반영, 금회설계, 증감 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[30rem]">구분</TableHead>
|
||||
@@ -36,7 +36,7 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) {
|
||||
|
||||
return (
|
||||
<TableRow key={node.cnstpSeCd}>
|
||||
<TableHead className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||
<TableHead scope="row" className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||
{node.cnstpSeCdNm}
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
|
||||
@@ -11,21 +11,21 @@ interface ContactInfoSectionProps {
|
||||
function ContactInfoSection({ det }: ContactInfoSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="담당자 정보">
|
||||
<Table variant="secondary">
|
||||
<Table caption="재원협의 담당자 정보" variant="secondary">
|
||||
<TableHeader>
|
||||
{det.aplcntNm && (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2}>신청자</TableHead>
|
||||
<TableHead>이름</TableHead>
|
||||
<TableHead scope="row" rowSpan={2}>신청자</TableHead>
|
||||
<TableHead scope="row">이름</TableHead>
|
||||
<TableCell>{det.aplcntNm || "-"}</TableCell>
|
||||
<TableHead>연락처</TableHead>
|
||||
<TableHead scope="row">연락처</TableHead>
|
||||
<TableCell>{det.aplcntMblTelno || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>소속</TableHead>
|
||||
<TableHead scope="row">소속</TableHead>
|
||||
<TableCell>{det.aplcntOgdpNm || "-"}</TableCell>
|
||||
<TableHead>이메일</TableHead>
|
||||
<TableHead scope="row">이메일</TableHead>
|
||||
<TableCell>{det.aplcntEmlAddr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
@@ -33,16 +33,16 @@ function ContactInfoSection({ det }: ContactInfoSectionProps) {
|
||||
{det.dsgnrNm && (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2}>설계자</TableHead>
|
||||
<TableHead>이름</TableHead>
|
||||
<TableHead scope="row" rowSpan={2}>설계자</TableHead>
|
||||
<TableHead scope="row">이름</TableHead>
|
||||
<TableCell>{det.dsgnrNm || "-"}</TableCell>
|
||||
<TableHead>연락처</TableHead>
|
||||
<TableHead scope="row">연락처</TableHead>
|
||||
<TableCell>{det.dsgnrTelno || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>소속</TableHead>
|
||||
<TableHead scope="row">소속</TableHead>
|
||||
<TableCell>{det.dsgnrOgdpNm || "-"}</TableCell>
|
||||
<TableHead>이메일</TableHead>
|
||||
<TableHead scope="row">이메일</TableHead>
|
||||
<TableCell>{det.dsgnrEmlAddr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
|
||||
@@ -21,7 +21,7 @@ function FnrsSharSection({ fnrsSharList }: FnrsSharSectionProps) {
|
||||
<SectionCollapsible title="재원분담">
|
||||
<div className="overflow-x-auto">
|
||||
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<Table caption="재원분담 내역: 구분, 현행, 변경요구, 증감, 비고 정보 제공" variant="secondary" className="table-fixed">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[24rem]" rowSpan={2}>
|
||||
@@ -51,7 +51,7 @@ function FnrsSharSection({ fnrsSharList }: FnrsSharSectionProps) {
|
||||
|
||||
return (
|
||||
<TableRow key={node.fnrsSharSeCd}>
|
||||
<TableHead className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||
<TableHead scope="row" className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||
{showSubIcon && <span className="mr-1 text-gray-400">└</span>}
|
||||
{node.fnrsSharSeCdNm}
|
||||
</TableHead>
|
||||
|
||||
@@ -189,7 +189,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||
<Table variant="secondary">
|
||||
<Table caption="검토의견 입력" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[12rem]">의견내용</TableHead>
|
||||
@@ -303,7 +303,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<Table caption="검토의견 목록: 의견자, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="55%" />
|
||||
|
||||
@@ -52,7 +52,7 @@ function ChecklistTaskPanel({
|
||||
return (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<Table caption="체크리스트 검토 입력: 검토항목, 주요 검토내용, 검토결과 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]" rowSpan={2}>
|
||||
|
||||
@@ -209,8 +209,11 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSave)} className="mt-4">
|
||||
<SectionCollapsibleGroup>
|
||||
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
|
||||
<div className="sticky top-[6rem] z-30 flex items-center justify-between bg-container px-4 py-2">
|
||||
<SectionCollapsibleToggle />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<span className="text-red-600">*</span>은 필수입력 항목입니다.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{initialData?.detDto.instlSncnDocNo && (
|
||||
|
||||
@@ -59,6 +59,7 @@ function InstlList({ searchParams, pagination, onPaginationChange }: InstlListPr
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "설치승인 목록",
|
||||
viewPagination: true,
|
||||
onRowClick: handleRowClick,
|
||||
},
|
||||
|
||||
@@ -53,7 +53,7 @@ function BasicInfoFormSection() {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<Table variant="secondary">
|
||||
<Table caption="설치승인 기본 정보 입력" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
@@ -62,7 +62,7 @@ function BasicInfoFormSection() {
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]" data-required>
|
||||
<TableHead scope="row" className="w-[20rem]" data-required>
|
||||
사업명
|
||||
</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
@@ -90,7 +90,7 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>사업기간</TableHead>
|
||||
<TableHead scope="row" data-required>사업기간</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<FieldGroup className="flex-row items-center gap-2">
|
||||
<Controller
|
||||
@@ -134,7 +134,7 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>사업내용</TableHead>
|
||||
<TableHead scope="row" data-required>사업내용</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<Controller
|
||||
name="instlSncnMng.bizCn"
|
||||
@@ -154,13 +154,13 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>구분</TableHead>
|
||||
<TableHead scope="row" data-required>구분</TableHead>
|
||||
<TableCell>{taskSeComCdLabel}</TableCell>
|
||||
<TableHead>신청 지자체</TableHead>
|
||||
<TableHead scope="row">신청 지자체</TableHead>
|
||||
<TableCell>{localGovLabel}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>사업구분</TableHead>
|
||||
<TableHead scope="row" data-required>사업구분</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<Controller
|
||||
name="instlSncnMng.sncnSeComCd"
|
||||
@@ -202,7 +202,7 @@ function BasicInfoFormSection() {
|
||||
{taskSeComCd.includes("RE") && (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead>시설종류</TableHead>
|
||||
<TableHead scope="row">시설종류</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<Controller
|
||||
name="instlSncnMng.rtlztnFcltSeComCd"
|
||||
@@ -242,7 +242,7 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>재이용용도</TableHead>
|
||||
<TableHead scope="row">재이용용도</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<Controller
|
||||
name="instlSncnMng.rtlztnUsgSeComCd"
|
||||
|
||||
@@ -11,7 +11,7 @@ interface BasicInfoSectionProps {
|
||||
function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<Table variant="secondary">
|
||||
<Table caption="설치승인 기본 정보" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
@@ -20,25 +20,25 @@ function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>사업명</TableHead>
|
||||
<TableHead scope="row">사업명</TableHead>
|
||||
<TableCell>{det.bizNm || "-"}</TableCell>
|
||||
<TableHead>사업기간</TableHead>
|
||||
<TableHead scope="row">사업기간</TableHead>
|
||||
<TableCell>{(det.bizBgngYr || "-") + " ~ " + (det.bizEndYr || "-")}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사업내용</TableHead>
|
||||
<TableHead scope="row">사업내용</TableHead>
|
||||
<TableCell colSpan={3}>{det.bizCn || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>구분</TableHead>
|
||||
<TableHead scope="row">구분</TableHead>
|
||||
<TableCell>{det.taskSeCdNm || "-"}</TableCell>
|
||||
<TableHead>신청지자체</TableHead>
|
||||
<TableHead scope="row">신청지자체</TableHead>
|
||||
<TableCell>
|
||||
{det.sido || "-"} {det.gugun || "-"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사업구분</TableHead>
|
||||
<TableHead scope="row">사업구분</TableHead>
|
||||
<TableCell colSpan={3}>{det.sncnSeCdNm || "-"}</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
@@ -20,7 +20,7 @@ function BusinessCostSection({ bizCstList }: BusinessCostSectionProps) {
|
||||
<SectionCollapsible title="총사업비">
|
||||
<div className="overflow-x-auto">
|
||||
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<Table caption="총사업비 내역: 구분, 예산반영, 표준사업비, 금회설계, 증감 정보 제공" variant="secondary" className="table-fixed">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[30rem]" rowSpan={2}>
|
||||
@@ -51,9 +51,9 @@ function BusinessCostSection({ bizCstList }: BusinessCostSectionProps) {
|
||||
return (
|
||||
<TableRow key={node.acntgSeCd}>
|
||||
{node.treeLevel <= 1 ? (
|
||||
<TableHead className="!px-[0.8rem] text-left">{node.acntgSeCdNm}</TableHead>
|
||||
<TableHead scope="row" className="!px-[0.8rem] text-left">{node.acntgSeCdNm}</TableHead>
|
||||
) : (
|
||||
<TableHead className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||
<TableHead scope="row" className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||
{showSubIcon && <span className="mr-1 text-gray-400">└</span>}
|
||||
{node.acntgSeCdNm}
|
||||
</TableHead>
|
||||
|
||||
@@ -13,7 +13,7 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="사업규모">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<Table caption="사업규모 내역: 구분, 기본계획, 예산반영, 금회설계, 증감 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[30rem]">구분</TableHead>
|
||||
@@ -36,7 +36,7 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) {
|
||||
|
||||
return (
|
||||
<TableRow key={node.cnstpSeCd}>
|
||||
<TableHead className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||
<TableHead scope="row" className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||
{node.cnstpSeCdNm}
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
|
||||
@@ -11,21 +11,21 @@ interface ContactInfoSectionProps {
|
||||
function ContactInfoSection({ det }: ContactInfoSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="담당자 정보">
|
||||
<Table variant="secondary">
|
||||
<Table caption="설치승인 담당자 정보" variant="secondary">
|
||||
<TableHeader>
|
||||
{det.aplcntNm && (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2}>신청자</TableHead>
|
||||
<TableHead>이름</TableHead>
|
||||
<TableHead scope="row" rowSpan={2}>신청자</TableHead>
|
||||
<TableHead scope="row">이름</TableHead>
|
||||
<TableCell>{det.aplcntNm || "-"}</TableCell>
|
||||
<TableHead>연락처</TableHead>
|
||||
<TableHead scope="row">연락처</TableHead>
|
||||
<TableCell>{det.aplcntMblTelno || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>소속</TableHead>
|
||||
<TableHead scope="row">소속</TableHead>
|
||||
<TableCell>{det.aplcntOgdpNm || "-"}</TableCell>
|
||||
<TableHead>이메일</TableHead>
|
||||
<TableHead scope="row">이메일</TableHead>
|
||||
<TableCell>{det.aplcntEmlAddr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
@@ -33,16 +33,16 @@ function ContactInfoSection({ det }: ContactInfoSectionProps) {
|
||||
{det.dsgnrNm && (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2}>설계자</TableHead>
|
||||
<TableHead>이름</TableHead>
|
||||
<TableHead scope="row" rowSpan={2}>설계자</TableHead>
|
||||
<TableHead scope="row">이름</TableHead>
|
||||
<TableCell>{det.dsgnrNm || "-"}</TableCell>
|
||||
<TableHead>연락처</TableHead>
|
||||
<TableHead scope="row">연락처</TableHead>
|
||||
<TableCell>{det.dsgnrTelno || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>소속</TableHead>
|
||||
<TableHead scope="row">소속</TableHead>
|
||||
<TableCell>{det.dsgnrOgdpNm || "-"}</TableCell>
|
||||
<TableHead>이메일</TableHead>
|
||||
<TableHead scope="row">이메일</TableHead>
|
||||
<TableCell>{det.dsgnrEmlAddr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
|
||||
@@ -180,10 +180,10 @@ const TechReviewOpinionSection = forwardRef<
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||
<Table variant="secondary">
|
||||
<Table caption="검토의견 입력" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[12rem]">의견내용</TableHead>
|
||||
<TableHead scope="row" className="w-[12rem]">의견내용</TableHead>
|
||||
<TableCell>
|
||||
<Textarea
|
||||
value={formData.rvwRsltCn}
|
||||
@@ -197,7 +197,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>검토결과</TableHead>
|
||||
<TableHead scope="row">검토결과</TableHead>
|
||||
<TableCell>
|
||||
<FieldSet>
|
||||
<RadioGroup
|
||||
@@ -234,7 +234,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>첨부파일</TableHead>
|
||||
<TableHead scope="row">첨부파일</TableHead>
|
||||
<TableCell>
|
||||
{uploadedFile ? (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -301,7 +301,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<Table caption="검토의견 목록: 의견자, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="55%" />
|
||||
|
||||
@@ -70,6 +70,7 @@ function MyPbadmsSprtList({ searchParams, pagination, onPaginationChange }: MyPb
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "나의 행정지원 목록",
|
||||
viewPagination: true,
|
||||
onRowClick: handleRowClick,
|
||||
},
|
||||
|
||||
@@ -97,6 +97,7 @@ function NtiHstryList({ searchParams, pagination, onPaginationChange }: NtiHstry
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "알림톡 발송 이력 목록",
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { NtitlkSndngPage } from "./ui/ntitlk-sndng-page";
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { SelectedNtitlkRcvr } from "../model/ntitlk-rcvr";
|
||||
|
||||
/** 템플릿 본문의 `#{변수}`를 값으로 치환한다. 값이 없는 변수는 빈 문자열이 된다. */
|
||||
export function buildTemplateMessage(
|
||||
templateContent: string,
|
||||
variables: Record<string, string>,
|
||||
): string {
|
||||
return templateContent.replace(/#?\{([^}]+)\}/g, (_, key: string) => variables[key.trim()] ?? "");
|
||||
}
|
||||
|
||||
export function getMergedVariables(
|
||||
recipient: SelectedNtitlkRcvr,
|
||||
commonValues: Record<string, string>,
|
||||
): Record<string, string> {
|
||||
return {
|
||||
...commonValues,
|
||||
...recipient.variableOverrides,
|
||||
TEL_NUM: recipient.telNo,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { PageResponse } from "@/shared/model";
|
||||
import type { InPrgrsDocPicResponseDto } from "@/entities/dashboard";
|
||||
import type { RvwMnpwMngResponseDto } from "@/entities/rvw-mnpw";
|
||||
|
||||
import type { NtitlkRcvr } from "../model/ntitlk-rcvr";
|
||||
|
||||
/** 검토인력은 결재구분별로 행이 나뉘어 내려오므로 사용자 기준으로 한 명씩만 남긴다. */
|
||||
export function selectRvwMnpwRcvrList(page: PageResponse<RvwMnpwMngResponseDto>): NtitlkRcvr[] {
|
||||
const rcvrByUserUuid = new Map<string, NtitlkRcvr>();
|
||||
|
||||
page.content.forEach((row) => {
|
||||
if (rcvrByUserUuid.has(row.userUuid)) return;
|
||||
|
||||
rcvrByUserUuid.set(row.userUuid, {
|
||||
rcvrKey: `rvw:${row.userUuid}`,
|
||||
userNm: row.userUuidNm ?? "",
|
||||
telNo: row.mblTelno ?? "",
|
||||
ognzNm: row.ognzNm,
|
||||
});
|
||||
});
|
||||
|
||||
return [...rcvrByUserUuid.values()];
|
||||
}
|
||||
|
||||
export function selectInPrgrsDocRcvrList(rows: InPrgrsDocPicResponseDto[]): NtitlkRcvr[] {
|
||||
return rows.map((row) => ({
|
||||
rcvrKey: `doc:${row.docSeComCd}:${row.docNo}`,
|
||||
userNm: row.aplcntNm ?? "",
|
||||
telNo: row.aplcntMblTelno ?? "",
|
||||
docNo: row.docNo,
|
||||
docNm: row.docNm,
|
||||
docSeNm: row.docSeNm,
|
||||
prgrsSttsNm: row.prgrsSttsNm,
|
||||
prcsDdln: row.prcsDdln?.slice(0, 10),
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
import type { OnChangeFn, Row, RowSelectionState } from "@tanstack/react-table";
|
||||
|
||||
import type { NtitlkRcvr, RcvrSelectionChange } from "../model/ntitlk-rcvr";
|
||||
|
||||
interface UseRcvrRowSelectionOptions {
|
||||
rcvrList: NtitlkRcvr[];
|
||||
selectedRcvrKeys: Set<string>;
|
||||
onRcvrSelectionChange: (change: RcvrSelectionChange) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 발송 목록(페이지 상태)을 수신자 테이블의 행 선택 상태로 연결한다.
|
||||
* 선택 상태를 테이블이 따로 들고 있지 않아, 발송 목록에서 제거하면 체크도 함께 풀린다.
|
||||
*/
|
||||
export function useRcvrRowSelection({
|
||||
rcvrList,
|
||||
selectedRcvrKeys,
|
||||
onRcvrSelectionChange,
|
||||
}: UseRcvrRowSelectionOptions) {
|
||||
const rowSelection = useMemo(() => {
|
||||
const selection: RowSelectionState = {};
|
||||
rcvrList.forEach((rcvr) => {
|
||||
if (selectedRcvrKeys.has(rcvr.rcvrKey)) selection[rcvr.rcvrKey] = true;
|
||||
});
|
||||
return selection;
|
||||
}, [rcvrList, selectedRcvrKeys]);
|
||||
|
||||
const handleRowSelectionChange: OnChangeFn<RowSelectionState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(rowSelection) : updater;
|
||||
|
||||
onRcvrSelectionChange({
|
||||
added: rcvrList.filter((rcvr) => next[rcvr.rcvrKey] && !rowSelection[rcvr.rcvrKey]),
|
||||
removedKeys: rcvrList
|
||||
.filter((rcvr) => !next[rcvr.rcvrKey] && rowSelection[rcvr.rcvrKey])
|
||||
.map((rcvr) => rcvr.rcvrKey),
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
selectedCount: Object.keys(rowSelection).length,
|
||||
rowSelectionOptions: {
|
||||
state: { rowSelection },
|
||||
// 전화번호가 없으면 발송할 수 없으므로 선택 대상에서 제외한다.
|
||||
enableRowSelection: (row: Row<NtitlkRcvr>) => Boolean(row.original.telNo),
|
||||
enableMultiRowSelection: true,
|
||||
getRowId: (rcvr: NtitlkRcvr) => rcvr.rcvrKey,
|
||||
onRowSelectionChange: handleRowSelectionChange,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/** 검토인력·문서 담당자 목록을 발송 목록 하나로 합치기 위한 수신자 모델 */
|
||||
export interface NtitlkRcvr {
|
||||
/** 행 식별자: 검토인력 `rvw:{userUuid}`, 문서 담당자 `doc:{docSeComCd}:{docNo}` */
|
||||
rcvrKey: string;
|
||||
userNm: string;
|
||||
telNo: string;
|
||||
ognzNm?: string;
|
||||
docNo?: string;
|
||||
docNm?: string;
|
||||
docSeNm?: string;
|
||||
prgrsSttsNm?: string;
|
||||
prcsDdln?: string;
|
||||
}
|
||||
|
||||
export interface SelectedNtitlkRcvr extends NtitlkRcvr {
|
||||
variableOverrides: Record<string, string>;
|
||||
}
|
||||
|
||||
/** 수신자 목록에서 선택 상태가 바뀐 행 */
|
||||
export interface RcvrSelectionChange {
|
||||
added: NtitlkRcvr[];
|
||||
removedKeys: string[];
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
"use client";
|
||||
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
|
||||
import { Checkbox } from "@common/components";
|
||||
|
||||
import type { NtitlkRcvr } from "./ntitlk-rcvr";
|
||||
|
||||
const columnHelper = createColumnHelper<NtitlkRcvr>();
|
||||
|
||||
function formatOptional(value: string | undefined) {
|
||||
return value?.trim() ? value : "-";
|
||||
}
|
||||
|
||||
const selectColumn = columnHelper.display({
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllRowsSelected()
|
||||
? true
|
||||
: table.getIsSomeRowsSelected()
|
||||
? "indeterminate"
|
||||
: false
|
||||
}
|
||||
onCheckedChange={(checked) => table.toggleAllRowsSelected(!!checked)}
|
||||
aria-label="전체 선택"
|
||||
/>
|
||||
),
|
||||
meta: { align: "center", width: 48 },
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
disabled={!row.getCanSelect()}
|
||||
onCheckedChange={(checked) => row.toggleSelected(!!checked)}
|
||||
aria-label="행 선택"
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
export const rvwMnpwRcvrColumns = [
|
||||
selectColumn,
|
||||
columnHelper.accessor("userNm", {
|
||||
header: "이름",
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("ognzNm", {
|
||||
header: "소속",
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("telNo", {
|
||||
header: "전화번호",
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
];
|
||||
|
||||
export const inPrgrsDocRcvrColumns = [
|
||||
selectColumn,
|
||||
columnHelper.accessor("docNo", {
|
||||
header: "문서번호",
|
||||
meta: { align: "left" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("docNm", {
|
||||
header: "문서명",
|
||||
meta: { align: "left" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("userNm", {
|
||||
header: "신청자",
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("prgrsSttsNm", {
|
||||
header: "상태",
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { NtitlkRcvr } from "./ntitlk-rcvr";
|
||||
|
||||
export const CUSTOM_VRBL_MAPPING = "custom";
|
||||
|
||||
export type VrblMappingKey = Exclude<keyof NtitlkRcvr, "rcvrKey"> | typeof CUSTOM_VRBL_MAPPING;
|
||||
|
||||
interface VrblMappingOption {
|
||||
value: VrblMappingKey;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/** 변수 일괄 적용 시 템플릿 변수에 채울 수신자 필드 */
|
||||
export const VRBL_MAPPING_OPTIONS: readonly VrblMappingOption[] = [
|
||||
{ value: "userNm", label: "이름" },
|
||||
{ value: "telNo", label: "전화번호" },
|
||||
{ value: "ognzNm", label: "소속" },
|
||||
{ value: "docNo", label: "문서번호" },
|
||||
{ value: "docNm", label: "문서명" },
|
||||
{ value: "docSeNm", label: "문서구분" },
|
||||
{ value: "prgrsSttsNm", label: "진행상태" },
|
||||
{ value: "prcsDdln", label: "처리기한" },
|
||||
{ value: CUSTOM_VRBL_MAPPING, label: "직접입력" },
|
||||
];
|
||||
@@ -0,0 +1,46 @@
|
||||
import { getCoreRowModel } from "@tanstack/react-table";
|
||||
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { useGetInPrgrsDocPicList } from "@/entities/dashboard";
|
||||
|
||||
import { selectInPrgrsDocRcvrList } from "../lib/to-ntitlk-rcvr";
|
||||
import { useRcvrRowSelection } from "../lib/use-rcvr-row-selection";
|
||||
import type { NtitlkRcvr, RcvrSelectionChange } from "../model/ntitlk-rcvr";
|
||||
import { inPrgrsDocRcvrColumns } from "../model/rcvr-table-columns";
|
||||
|
||||
import { RcvrListCard } from "./rcvr-list-card";
|
||||
|
||||
const EMPTY_RCVR_LIST: NtitlkRcvr[] = [];
|
||||
|
||||
interface InPrgrsDocRcvrListProps {
|
||||
selectedRcvrKeys: Set<string>;
|
||||
onRcvrSelectionChange: (change: RcvrSelectionChange) => void;
|
||||
}
|
||||
|
||||
function InPrgrsDocRcvrList({ selectedRcvrKeys, onRcvrSelectionChange }: InPrgrsDocRcvrListProps) {
|
||||
const rcvrQuery = useGetInPrgrsDocPicList(undefined, { select: selectInPrgrsDocRcvrList });
|
||||
|
||||
const { selectedCount, rowSelectionOptions } = useRcvrRowSelection({
|
||||
rcvrList: rcvrQuery.data ?? EMPTY_RCVR_LIST,
|
||||
selectedRcvrKeys,
|
||||
onRcvrSelectionChange,
|
||||
});
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
query: rcvrQuery,
|
||||
tableOptions: {
|
||||
columns: inPrgrsDocRcvrColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
...rowSelectionOptions,
|
||||
},
|
||||
dataTableOptions: { caption: "진행중 문서 담당자 수신자 목록" },
|
||||
});
|
||||
|
||||
return (
|
||||
<RcvrListCard title="현재 진행중 문서 담당자(신청자)" selectedCount={selectedCount}>
|
||||
{tableComponent}
|
||||
</RcvrListCard>
|
||||
);
|
||||
}
|
||||
|
||||
export { InPrgrsDocRcvrList };
|
||||
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@common/components";
|
||||
|
||||
import type { SelectedNtitlkRcvr } from "../model/ntitlk-rcvr";
|
||||
|
||||
import { NtitlkPrvw } from "./ntitlk-prvw";
|
||||
|
||||
interface NtitlkPrvwDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
recipient: SelectedNtitlkRcvr | null;
|
||||
message: string;
|
||||
}
|
||||
|
||||
function NtitlkPrvwDialog({ open, onOpenChange, recipient, message }: NtitlkPrvwDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[36rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>알림톡 미리보기</DialogTitle>
|
||||
<DialogDescription className="text-[1.1rem] tracking-tight">
|
||||
수신자: {recipient?.userNm || "-"} / {recipient?.telNo || "-"}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<NtitlkPrvw message={message || "메시지를 생성할 수 없습니다."} />
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" color="white">
|
||||
닫기
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { NtitlkPrvwDialog };
|
||||
@@ -0,0 +1,25 @@
|
||||
const SNDR_NM = "디지털 행정지원 시스템";
|
||||
|
||||
interface NtitlkPrvwProps {
|
||||
message: string;
|
||||
}
|
||||
|
||||
function NtitlkPrvw({ message }: NtitlkPrvwProps) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="h-[32rem] overflow-auto rounded-[20px] rounded-b-none bg-[rgb(176,192,207)] p-8 pb-0 before:absolute before:left-0 before:top-0 before:block before:h-full before:w-full before:rounded-[20px] before:rounded-b-none before:border-[10px] before:border-b-0 before:border-black before:content-['']">
|
||||
<div className="w-[calc(100%-2rem)] pb-5">
|
||||
<span className="text-[1.1rem] text-black">{SNDR_NM}</span>
|
||||
<div className="mt-2 rounded-t-[12px] bg-[rgb(247,228,54)] p-4 text-[1.2rem] font-medium text-black">
|
||||
알림톡 도착
|
||||
</div>
|
||||
<div className="whitespace-pre-wrap rounded-b-[12px] bg-white p-4 text-[1.2rem] leading-normal tracking-tight text-black">
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { NtitlkPrvw };
|
||||
@@ -0,0 +1,266 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import {
|
||||
type NtftalkTemplate,
|
||||
useNtftalkTemplateList,
|
||||
useNtftalkTemplateVariables,
|
||||
useSendNtftalkBusiness,
|
||||
} from "@/entities/ntftalk";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import { buildTemplateMessage, getMergedVariables } from "../lib/build-tmplt-msg";
|
||||
import type { RcvrSelectionChange, SelectedNtitlkRcvr } from "../model/ntitlk-rcvr";
|
||||
import { CUSTOM_VRBL_MAPPING, type VrblMappingKey } from "../model/vrbl-mapping-options";
|
||||
|
||||
import { InPrgrsDocRcvrList } from "./in-prgrs-doc-rcvr-list";
|
||||
import { NtitlkPrvwDialog } from "./ntitlk-prvw-dialog";
|
||||
import { RvwMnpwRcvrList } from "./rvw-mnpw-rcvr-list";
|
||||
import { SndngList } from "./sndng-list";
|
||||
import { TmpltSelectPanel } from "./tmplt-select-panel";
|
||||
import { VrblBulkApply } from "./vrbl-bulk-apply";
|
||||
|
||||
const SNDNG_CALLBACK_TELNO = "032-590-4000";
|
||||
const EMPTY_TEMPLATES: NtftalkTemplate[] = [];
|
||||
const EMPTY_VARIABLE_NAMES: string[] = [];
|
||||
|
||||
function NtitlkSndngPage() {
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
const [selectedTemplateCode, setSelectedTemplateCode] = useState("");
|
||||
const [selectedRecipients, setSelectedRecipients] = useState<SelectedNtitlkRcvr[]>([]);
|
||||
const [commonVariableValues, setCommonVariableValues] = useState<Record<string, string>>({});
|
||||
const [commonVariableMapping, setCommonVariableMapping] = useState<
|
||||
Record<string, VrblMappingKey>
|
||||
>({});
|
||||
const [previewRecipient, setPreviewRecipient] = useState<SelectedNtitlkRcvr | null>(null);
|
||||
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
||||
|
||||
const { data: templatePage } = useNtftalkTemplateList();
|
||||
const templates = templatePage?.content ?? EMPTY_TEMPLATES;
|
||||
const selectedTemplate = templates.find(
|
||||
(template) => template.templateCode === selectedTemplateCode,
|
||||
);
|
||||
|
||||
const { data: templateVariables } = useNtftalkTemplateVariables(selectedTemplateCode);
|
||||
const variableNames = templateVariables?.variables ?? EMPTY_VARIABLE_NAMES;
|
||||
|
||||
const { mutate: sendNtftalk, isPending: isSending } = useSendNtftalkBusiness();
|
||||
|
||||
const selectedRcvrKeys = useMemo(
|
||||
() => new Set(selectedRecipients.map((recipient) => recipient.rcvrKey)),
|
||||
[selectedRecipients],
|
||||
);
|
||||
|
||||
const previewMessage =
|
||||
previewRecipient && selectedTemplate?.templateContent
|
||||
? buildTemplateMessage(
|
||||
selectedTemplate.templateContent,
|
||||
getMergedVariables(previewRecipient, commonVariableValues),
|
||||
)
|
||||
: "";
|
||||
|
||||
const handleTemplateChange = (templateCode: string) => {
|
||||
setSelectedTemplateCode(templateCode);
|
||||
setCommonVariableValues({});
|
||||
setCommonVariableMapping({});
|
||||
setSelectedRecipients((prev) =>
|
||||
prev.map((recipient) => ({ ...recipient, variableOverrides: {} })),
|
||||
);
|
||||
};
|
||||
|
||||
const handleRcvrSelectionChange = ({ added, removedKeys }: RcvrSelectionChange) => {
|
||||
setSelectedRecipients((prev) => {
|
||||
const removedKeySet = new Set(removedKeys);
|
||||
const existingKeySet = new Set(prev.map((recipient) => recipient.rcvrKey));
|
||||
|
||||
return [
|
||||
...prev.filter((recipient) => !removedKeySet.has(recipient.rcvrKey)),
|
||||
...added
|
||||
.filter((rcvr) => !existingKeySet.has(rcvr.rcvrKey))
|
||||
.map((rcvr) => ({ ...rcvr, variableOverrides: {} })),
|
||||
];
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveRecipient = (rcvrKey: string) => {
|
||||
setSelectedRecipients((prev) => prev.filter((recipient) => recipient.rcvrKey !== rcvrKey));
|
||||
};
|
||||
|
||||
const handleApplyCommonValues = () => {
|
||||
setSelectedRecipients((prev) =>
|
||||
prev.map((recipient) => {
|
||||
const newOverrides: Record<string, string> = {};
|
||||
|
||||
variableNames.forEach((variableName) => {
|
||||
const mappingKey = commonVariableMapping[variableName];
|
||||
if (!mappingKey) return;
|
||||
|
||||
newOverrides[variableName] =
|
||||
mappingKey === CUSTOM_VRBL_MAPPING
|
||||
? (commonVariableValues[variableName] ?? "")
|
||||
: (recipient[mappingKey] ?? "");
|
||||
});
|
||||
|
||||
return {
|
||||
...recipient,
|
||||
variableOverrides: { ...recipient.variableOverrides, ...newOverrides },
|
||||
};
|
||||
}),
|
||||
);
|
||||
setCommonVariableMapping({});
|
||||
};
|
||||
|
||||
const handleResetCommonValues = () => {
|
||||
setCommonVariableMapping({});
|
||||
setCommonVariableValues({});
|
||||
};
|
||||
|
||||
const handleRecipientVariableChange = (rcvrKey: string, variableName: string, value: string) => {
|
||||
setSelectedRecipients((prev) =>
|
||||
prev.map((recipient) =>
|
||||
recipient.rcvrKey === rcvrKey
|
||||
? {
|
||||
...recipient,
|
||||
variableOverrides: { ...recipient.variableOverrides, [variableName]: value },
|
||||
}
|
||||
: recipient,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const handlePreview = (recipient: SelectedNtitlkRcvr) => {
|
||||
if (!selectedTemplate) {
|
||||
alert("알림톡 템플릿을 먼저 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
setPreviewRecipient(recipient);
|
||||
setIsPreviewOpen(true);
|
||||
};
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!selectedTemplateCode) {
|
||||
alert("알림톡 템플릿을 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const hasMissingVariable = selectedRecipients.some((recipient) =>
|
||||
variableNames.some((variableName) => {
|
||||
const mergedValue =
|
||||
recipient.variableOverrides[variableName] ?? commonVariableValues[variableName] ?? "";
|
||||
return mergedValue.trim() === "";
|
||||
}),
|
||||
);
|
||||
if (hasMissingVariable) {
|
||||
alert("변수값이 입력되지 않은 발송 대상이 있습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await confirm(`${selectedRecipients.length}명에게 발송하시겠습니까?`))) return;
|
||||
|
||||
sendNtftalk(
|
||||
{
|
||||
templateCode: selectedTemplateCode,
|
||||
callback: SNDNG_CALLBACK_TELNO,
|
||||
recipients: selectedRecipients.map((recipient) =>
|
||||
getMergedVariables(recipient, commonVariableValues),
|
||||
),
|
||||
},
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
if (!data.success) {
|
||||
alert(data.message || "알림톡 발송 요청에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
alert(data.message || "알림톡 발송이 요청되었습니다.");
|
||||
setSelectedRecipients([]);
|
||||
setCommonVariableValues({});
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 px-[1.8rem] pb-6">
|
||||
<div className="mt-4 grid grid-cols-1 gap-4 xl:grid-cols-[32rem_minmax(0,1fr)]">
|
||||
<TmpltSelectPanel
|
||||
templates={templates}
|
||||
selectedTemplate={selectedTemplate}
|
||||
onTemplateChange={handleTemplateChange}
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<RvwMnpwRcvrList
|
||||
selectedRcvrKeys={selectedRcvrKeys}
|
||||
onRcvrSelectionChange={handleRcvrSelectionChange}
|
||||
/>
|
||||
<InPrgrsDocRcvrList
|
||||
selectedRcvrKeys={selectedRcvrKeys}
|
||||
onRcvrSelectionChange={handleRcvrSelectionChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="card card-border m-0 bg-background p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h3 className="text-xl font-semibold">발송 목록</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
onClick={() => setSelectedRecipients([])}
|
||||
disabled={selectedRecipients.length === 0}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="green"
|
||||
onClick={handleSend}
|
||||
disabled={isSending || selectedRecipients.length === 0}
|
||||
>
|
||||
발송
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{variableNames.length > 0 && (
|
||||
<VrblBulkApply
|
||||
variableNames={variableNames}
|
||||
mapping={commonVariableMapping}
|
||||
customValues={commonVariableValues}
|
||||
onMappingChange={(variableName, mappingKey) =>
|
||||
setCommonVariableMapping((prev) => ({ ...prev, [variableName]: mappingKey }))
|
||||
}
|
||||
onCustomValueChange={(variableName, value) =>
|
||||
setCommonVariableValues((prev) => ({ ...prev, [variableName]: value }))
|
||||
}
|
||||
onApply={handleApplyCommonValues}
|
||||
onReset={handleResetCommonValues}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SndngList
|
||||
recipients={selectedRecipients}
|
||||
variableNames={variableNames}
|
||||
onVariableChange={handleRecipientVariableChange}
|
||||
onPreview={handlePreview}
|
||||
onRemove={handleRemoveRecipient}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<NtitlkPrvwDialog
|
||||
open={isPreviewOpen}
|
||||
onOpenChange={setIsPreviewOpen}
|
||||
recipient={previewRecipient}
|
||||
message={previewMessage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { NtitlkSndngPage };
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface RcvrListCardProps {
|
||||
title: string;
|
||||
selectedCount: number;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function RcvrListCard({ title, selectedCount, children }: RcvrListCardProps) {
|
||||
return (
|
||||
<section className="card card-border m-0 bg-background p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h3 className="text-xl font-semibold">{title}</h3>
|
||||
<p className="text-muted-foreground">
|
||||
<span className="font-semibold text-foreground">{selectedCount}명</span> 선택
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-[16rem] overflow-auto rounded-md border">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export { RcvrListCard };
|
||||
@@ -0,0 +1,47 @@
|
||||
import { getCoreRowModel } from "@tanstack/react-table";
|
||||
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { useRvwMnpwMngPage } from "@/entities/rvw-mnpw";
|
||||
|
||||
import { selectRvwMnpwRcvrList } from "../lib/to-ntitlk-rcvr";
|
||||
import { useRcvrRowSelection } from "../lib/use-rcvr-row-selection";
|
||||
import type { NtitlkRcvr, RcvrSelectionChange } from "../model/ntitlk-rcvr";
|
||||
import { rvwMnpwRcvrColumns } from "../model/rcvr-table-columns";
|
||||
|
||||
import { RcvrListCard } from "./rcvr-list-card";
|
||||
|
||||
const RVW_MNPW_SEARCH = { page: 0, size: 1000, sort: "userUuidNm,asc" };
|
||||
const EMPTY_RCVR_LIST: NtitlkRcvr[] = [];
|
||||
|
||||
interface RvwMnpwRcvrListProps {
|
||||
selectedRcvrKeys: Set<string>;
|
||||
onRcvrSelectionChange: (change: RcvrSelectionChange) => void;
|
||||
}
|
||||
|
||||
function RvwMnpwRcvrList({ selectedRcvrKeys, onRcvrSelectionChange }: RvwMnpwRcvrListProps) {
|
||||
const rcvrQuery = useRvwMnpwMngPage(RVW_MNPW_SEARCH, { select: selectRvwMnpwRcvrList });
|
||||
|
||||
const { selectedCount, rowSelectionOptions } = useRcvrRowSelection({
|
||||
rcvrList: rcvrQuery.data ?? EMPTY_RCVR_LIST,
|
||||
selectedRcvrKeys,
|
||||
onRcvrSelectionChange,
|
||||
});
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
query: rcvrQuery,
|
||||
tableOptions: {
|
||||
columns: rvwMnpwRcvrColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
...rowSelectionOptions,
|
||||
},
|
||||
dataTableOptions: { caption: "검토인력 수신자 목록" },
|
||||
});
|
||||
|
||||
return (
|
||||
<RcvrListCard title="검토인력 목록" selectedCount={selectedCount}>
|
||||
{tableComponent}
|
||||
</RcvrListCard>
|
||||
);
|
||||
}
|
||||
|
||||
export { RvwMnpwRcvrList };
|
||||
@@ -0,0 +1,90 @@
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
import type { SelectedNtitlkRcvr } from "../model/ntitlk-rcvr";
|
||||
|
||||
interface SndngListProps {
|
||||
recipients: SelectedNtitlkRcvr[];
|
||||
variableNames: string[];
|
||||
onVariableChange: (rcvrKey: string, variableName: string, value: string) => void;
|
||||
onPreview: (recipient: SelectedNtitlkRcvr) => void;
|
||||
onRemove: (rcvrKey: string) => void;
|
||||
}
|
||||
|
||||
function SndngList({
|
||||
recipients,
|
||||
variableNames,
|
||||
onVariableChange,
|
||||
onPreview,
|
||||
onRemove,
|
||||
}: SndngListProps) {
|
||||
if (recipients.length === 0)
|
||||
return (
|
||||
<p className="rounded-md border p-6 text-center text-muted-foreground">
|
||||
선택된 발송 대상이 없습니다.
|
||||
</p>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="max-h-[40rem] overflow-auto rounded-md border">
|
||||
<Table caption="알림톡 발송 대상 목록: 이름, 전화번호, 변수, 버튼 정보 제공" variant="line" className="text-sm">
|
||||
<TableHeader className="top-0">
|
||||
<TableRow>
|
||||
<TableHead className="min-w-[100px]">이름</TableHead>
|
||||
<TableHead className="min-w-[120px]">전화번호</TableHead>
|
||||
{variableNames.map((variableName) => (
|
||||
<TableHead key={variableName} className="min-w-[180px]">
|
||||
#{variableName}
|
||||
</TableHead>
|
||||
))}
|
||||
<TableHead className="min-w-[160px] text-center">버튼</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{recipients.map((recipient) => (
|
||||
<TableRow key={recipient.rcvrKey}>
|
||||
<TableCell align="center">{recipient.userNm}</TableCell>
|
||||
<TableCell align="center">{recipient.telNo}</TableCell>
|
||||
{variableNames.map((variableName) => (
|
||||
<TableCell key={variableName}>
|
||||
<Input
|
||||
value={recipient.variableOverrides[variableName] ?? ""}
|
||||
onChange={(event) =>
|
||||
onVariableChange(recipient.rcvrKey, variableName, event.target.value)
|
||||
}
|
||||
className="w-full min-w-[150px]"
|
||||
/>
|
||||
</TableCell>
|
||||
))}
|
||||
<TableCell align="center">
|
||||
<div className="flex justify-center gap-2">
|
||||
<Button type="button" size="sm" color="gray" onClick={() => onPreview(recipient)}>
|
||||
미리보기
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="red"
|
||||
onClick={() => onRemove(recipient.rcvrKey)}
|
||||
>
|
||||
제거
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { SndngList };
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { NtftalkTemplate } from "@/entities/ntftalk";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@common/components";
|
||||
|
||||
import { NtitlkPrvw } from "./ntitlk-prvw";
|
||||
|
||||
interface TmpltSelectPanelProps {
|
||||
templates: NtftalkTemplate[];
|
||||
selectedTemplate: NtftalkTemplate | undefined;
|
||||
onTemplateChange: (templateCode: string) => void;
|
||||
}
|
||||
|
||||
function TmpltSelectPanel({
|
||||
templates,
|
||||
selectedTemplate,
|
||||
onTemplateChange,
|
||||
}: TmpltSelectPanelProps) {
|
||||
return (
|
||||
<section className="card card-border m-0 space-y-6 bg-background p-4 pb-0">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-semibold">알림톡 템플릿 선택</h3>
|
||||
<Select value={selectedTemplate?.templateCode ?? ""} onValueChange={onTemplateChange}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="템플릿 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{templates.map((template) => (
|
||||
<SelectItem key={template.templateCode} value={template.templateCode}>
|
||||
{template.templateName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className="text-lg font-medium">미리보기</p>
|
||||
<NtitlkPrvw
|
||||
message={
|
||||
selectedTemplate?.templateContent ??
|
||||
"알림톡 템플릿을 선택하면 미리보기 메시지가 표시됩니다."
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export { TmpltSelectPanel };
|
||||
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
|
||||
import {
|
||||
CUSTOM_VRBL_MAPPING,
|
||||
VRBL_MAPPING_OPTIONS,
|
||||
type VrblMappingKey,
|
||||
} from "../model/vrbl-mapping-options";
|
||||
|
||||
interface VrblBulkApplyProps {
|
||||
variableNames: string[];
|
||||
mapping: Record<string, VrblMappingKey>;
|
||||
customValues: Record<string, string>;
|
||||
onMappingChange: (variableName: string, mappingKey: VrblMappingKey) => void;
|
||||
onCustomValueChange: (variableName: string, value: string) => void;
|
||||
onApply: () => void;
|
||||
onReset: () => void;
|
||||
}
|
||||
|
||||
function VrblBulkApply({
|
||||
variableNames,
|
||||
mapping,
|
||||
customValues,
|
||||
onMappingChange,
|
||||
onCustomValueChange,
|
||||
onApply,
|
||||
onReset,
|
||||
}: VrblBulkApplyProps) {
|
||||
return (
|
||||
<div className="mb-4 space-y-4 rounded-md border p-3">
|
||||
<p className="text-lg font-medium">변수 일괄 적용</p>
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-6">
|
||||
{variableNames.map((variableName) => (
|
||||
<div key={variableName} className="space-y-2">
|
||||
<Label className="whitespace-nowrap">{`#{${variableName}}`}</Label>
|
||||
<Select
|
||||
value={mapping[variableName] ?? ""}
|
||||
onValueChange={(value) => onMappingChange(variableName, value as VrblMappingKey)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{VRBL_MAPPING_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{mapping[variableName] === CUSTOM_VRBL_MAPPING && (
|
||||
<Input
|
||||
placeholder="값을 입력하세요"
|
||||
value={customValues[variableName] ?? ""}
|
||||
onChange={(event) => onCustomValueChange(variableName, event.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-end gap-2">
|
||||
<Button type="button" color="white" onClick={onReset}>
|
||||
초기화
|
||||
</Button>
|
||||
<Button type="button" onClick={onApply}>
|
||||
적용하기
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { VrblBulkApply };
|
||||
@@ -52,7 +52,7 @@ function ChecklistTaskPanel({
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<Table caption="체크리스트 검토 입력: 검토항목, 주요 검토내용, 검토결과 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]" rowSpan={2}>
|
||||
|
||||
@@ -35,7 +35,7 @@ function PreviousPlanAreaTable({ data }: PreviousPlanAreaTableProps) {
|
||||
const yearCells = getPlanYearCells(data);
|
||||
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<Table caption="이전 기본계획 처리구역 현황" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="16%" />
|
||||
<col width="9.3%" />
|
||||
@@ -86,7 +86,7 @@ function PreviousPlanAreaTable({ data }: PreviousPlanAreaTableProps) {
|
||||
const cells = getMetricCells(data, row.field);
|
||||
return (
|
||||
<TableRow key={row.field}>
|
||||
<TableHead>{row.label}</TableHead>
|
||||
<TableHead scope="row">{row.label}</TableHead>
|
||||
{cells.map((cell, index) => (
|
||||
<TableCell key={index} className="text-right">
|
||||
{cell}
|
||||
|
||||
@@ -202,7 +202,7 @@ function PreviousPlanFileSection({ clsfSystmNm }: PreviousPlanFileSectionProps)
|
||||
) : files.length === 0 ? (
|
||||
<p className="py-8 text-center text-lg text-muted-foreground">파일이 없습니다.</p>
|
||||
) : (
|
||||
<Table variant="secondary">
|
||||
<Table caption="이전 기본계획 첨부파일 목록" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="!py-2 !text-xl">번호</TableHead>
|
||||
|
||||
@@ -13,7 +13,7 @@ function PreviousPlanSummaryTable({ data }: PreviousPlanSummaryTableProps) {
|
||||
const yearCells = getPlanYearCells(data);
|
||||
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<Table caption="이전 기본계획 목표연도" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
@@ -54,7 +54,7 @@ function PreviousPlanSummaryTable({ data }: PreviousPlanSummaryTableProps) {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>계획목표년도</TableHead>
|
||||
<TableHead scope="row">계획목표년도</TableHead>
|
||||
{yearCells.map((cell, index) => (
|
||||
<TableCell key={index} className="text-center">
|
||||
{cell}
|
||||
|
||||
@@ -57,7 +57,7 @@ function BasicInfoFormSection() {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="기본계획 신청">
|
||||
<Table variant="secondary">
|
||||
<Table caption="기본계획 정보 입력" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
@@ -66,7 +66,7 @@ function BasicInfoFormSection() {
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]" data-required>
|
||||
<TableHead scope="row" className="w-[20rem]" data-required>
|
||||
기본계획명
|
||||
</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
@@ -89,7 +89,7 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>계획년도</TableHead>
|
||||
<TableHead scope="row" data-required>계획년도</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="sewerBscPlanDtl.planYr"
|
||||
@@ -113,7 +113,7 @@ function BasicInfoFormSection() {
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableHead data-required>기준년도</TableHead>
|
||||
<TableHead scope="row" data-required>기준년도</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="sewerBscPlanDtl.crtrYr"
|
||||
@@ -139,7 +139,7 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>1단계 목표연도</TableHead>
|
||||
<TableHead scope="row" data-required>1단계 목표연도</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="sewerBscPlanDtl.frlvlGoalYr"
|
||||
@@ -166,7 +166,7 @@ function BasicInfoFormSection() {
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableHead data-required>2단계 목표연도</TableHead>
|
||||
<TableHead scope="row" data-required>2단계 목표연도</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="sewerBscPlanDtl.sclvlGoalYr"
|
||||
@@ -192,7 +192,7 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>3단계 목표연도</TableHead>
|
||||
<TableHead scope="row" data-required>3단계 목표연도</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="sewerBscPlanDtl.thlvlGoalYr"
|
||||
@@ -216,7 +216,7 @@ function BasicInfoFormSection() {
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableHead data-required>4단계 목표연도</TableHead>
|
||||
<TableHead scope="row" data-required>4단계 목표연도</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="sewerBscPlanDtl.folvlGoalYr"
|
||||
@@ -242,7 +242,7 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>계획개요</TableHead>
|
||||
<TableHead scope="row" data-required>계획개요</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<Controller
|
||||
name="sewerBscPlanDtl.bizOtlnCn"
|
||||
@@ -264,9 +264,9 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>구분</TableHead>
|
||||
<TableHead scope="row">구분</TableHead>
|
||||
<TableCell>{sewerBscPlanSeLabel}</TableCell>
|
||||
<TableHead>신청 지자체</TableHead>
|
||||
<TableHead scope="row">신청 지자체</TableHead>
|
||||
<TableCell>{localGovLabel || userInfo?.laeSeNm}</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
@@ -18,7 +18,7 @@ function BasicInfoSection({ det, dtl }: BasicInfoSectionProps) {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="기본계획 신청">
|
||||
<Table variant="secondary">
|
||||
<Table caption="기본계획 정보" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
@@ -27,37 +27,37 @@ function BasicInfoSection({ det, dtl }: BasicInfoSectionProps) {
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>기본계획명</TableHead>
|
||||
<TableHead scope="row">기본계획명</TableHead>
|
||||
<TableCell colSpan={3}>{det.sewerBscPlanNm || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>계획년도</TableHead>
|
||||
<TableHead scope="row">계획년도</TableHead>
|
||||
<TableCell>{dtl?.planYr || "-"}</TableCell>
|
||||
<TableHead>기준년도</TableHead>
|
||||
<TableHead scope="row">기준년도</TableHead>
|
||||
<TableCell>{dtl?.crtrYr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>1단계 목표연도</TableHead>
|
||||
<TableHead scope="row">1단계 목표연도</TableHead>
|
||||
<TableCell>{dtl?.frlvlGoalYr || "-"}</TableCell>
|
||||
<TableHead>2단계 목표연도</TableHead>
|
||||
<TableHead scope="row">2단계 목표연도</TableHead>
|
||||
<TableCell>{dtl?.sclvlGoalYr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>3단계 목표연도</TableHead>
|
||||
<TableHead scope="row">3단계 목표연도</TableHead>
|
||||
<TableCell>{dtl?.thlvlGoalYr || "-"}</TableCell>
|
||||
<TableHead>4단계 목표연도</TableHead>
|
||||
<TableHead scope="row">4단계 목표연도</TableHead>
|
||||
<TableCell>{dtl?.folvlGoalYr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>계획개요</TableHead>
|
||||
<TableHead scope="row">계획개요</TableHead>
|
||||
<TableCell colSpan={3} className="whitespace-pre-wrap">
|
||||
{dtl?.bizOtlnCn || "-"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>구분</TableHead>
|
||||
<TableHead scope="row">구분</TableHead>
|
||||
<TableCell>{sewerBscPlanSeLabel || "-"}</TableCell>
|
||||
<TableHead>신청 지자체</TableHead>
|
||||
<TableHead scope="row">신청 지자체</TableHead>
|
||||
<TableCell>{localGovLabel || "-"}</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useFormContext, useWatch } from "react-hook-form";
|
||||
|
||||
import { NON_NEGATIVE_INPUT_PROPS, stripMinusSign } from "@/shared/lib";
|
||||
import { NON_NEGATIVE_INPUT_PROPS, sanitizeNumberInput } from "@/shared/lib";
|
||||
import { SectionCollapsible } from "@/shared/ui";
|
||||
import { Field } from "@/shared/ui/field";
|
||||
import { useAcntgSeMngTree } from "@/entities/acntg";
|
||||
@@ -51,7 +51,7 @@ function BizCstMatrixFormSection() {
|
||||
];
|
||||
|
||||
function handleChange(acntgSeCd: string, planStpNo: number, value: string) {
|
||||
const normalized = truncateDecimals(stripMinusSign(value), 3);
|
||||
const normalized = truncateDecimals(sanitizeNumberInput(value), 3);
|
||||
const numValue = Number(normalized.replace(/,/g, "")) || 0;
|
||||
const next = recalculateBizCstMatrix(
|
||||
setAmt(getValues("bizCstList"), acntgSeCd, planStpNo, numValue),
|
||||
@@ -75,7 +75,7 @@ function BizCstMatrixFormSection() {
|
||||
<SectionCollapsible title="총사업비">
|
||||
<div className="overflow-x-auto">
|
||||
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<Table caption="단계별 총사업비 입력" variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="22%" />
|
||||
@@ -102,7 +102,7 @@ function BizCstMatrixFormSection() {
|
||||
|
||||
return (
|
||||
<TableRow key={item.code}>
|
||||
<TableHead className="text-left" style={{ paddingLeft }}>
|
||||
<TableHead scope="row" className="text-left" style={{ paddingLeft }}>
|
||||
<div className="relative">
|
||||
{showSubIcon && (
|
||||
<span className="absolute -left-6 top-0 text-gray-400">└</span>
|
||||
|
||||
@@ -62,7 +62,7 @@ function BizCstMatrixSection({ bizCstList, dtl }: BizCstMatrixSectionProps) {
|
||||
<SectionCollapsible title="총사업비">
|
||||
<div className="overflow-x-auto">
|
||||
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<Table caption="단계별 총사업비 내역" variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="22%" />
|
||||
@@ -89,7 +89,7 @@ function BizCstMatrixSection({ bizCstList, dtl }: BizCstMatrixSectionProps) {
|
||||
|
||||
return (
|
||||
<TableRow key={item.code}>
|
||||
<TableHead className="text-left" style={{ paddingLeft }}>
|
||||
<TableHead scope="row" className="text-left" style={{ paddingLeft }}>
|
||||
<div className="relative">
|
||||
{showSubIcon && (
|
||||
<span className="absolute -left-6 top-0 text-gray-400">└</span>
|
||||
|
||||
@@ -13,21 +13,21 @@ function ContactInfoSection({ dtl }: ContactInfoSectionProps) {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="담당자 정보">
|
||||
<Table variant="secondary">
|
||||
<Table caption="기본계획 담당자 정보" variant="secondary">
|
||||
<TableHeader>
|
||||
{dtl.aplcntNm ? (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2}>신청자</TableHead>
|
||||
<TableHead>이름</TableHead>
|
||||
<TableHead scope="row" rowSpan={2}>신청자</TableHead>
|
||||
<TableHead scope="row">이름</TableHead>
|
||||
<TableCell>{dtl.aplcntNm || "-"}</TableCell>
|
||||
<TableHead>연락처</TableHead>
|
||||
<TableHead scope="row">연락처</TableHead>
|
||||
<TableCell>{dtl.aplcntMblTelno || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>소속</TableHead>
|
||||
<TableHead scope="row">소속</TableHead>
|
||||
<TableCell>{dtl.aplcntOgdpNm || "-"}</TableCell>
|
||||
<TableHead>이메일</TableHead>
|
||||
<TableHead scope="row">이메일</TableHead>
|
||||
<TableCell>{dtl.aplcntEmlAddr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
@@ -35,16 +35,16 @@ function ContactInfoSection({ dtl }: ContactInfoSectionProps) {
|
||||
{dtl.dsgnrNm ? (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2}>설계자</TableHead>
|
||||
<TableHead>이름</TableHead>
|
||||
<TableHead scope="row" rowSpan={2}>설계자</TableHead>
|
||||
<TableHead scope="row">이름</TableHead>
|
||||
<TableCell>{dtl.dsgnrNm || "-"}</TableCell>
|
||||
<TableHead>연락처</TableHead>
|
||||
<TableHead scope="row">연락처</TableHead>
|
||||
<TableCell>{dtl.dsgnrTelno || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>소속</TableHead>
|
||||
<TableHead scope="row">소속</TableHead>
|
||||
<TableCell>{dtl.dsgnrOgdpNm || "-"}</TableCell>
|
||||
<TableHead>이메일</TableHead>
|
||||
<TableHead scope="row">이메일</TableHead>
|
||||
<TableCell>{dtl.dsgnrEmlAddr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useMemo } from "react";
|
||||
import { useFormContext, useWatch } from "react-hook-form";
|
||||
|
||||
import { NON_NEGATIVE_INPUT_PROPS, stripMinusSign } from "@/shared/lib";
|
||||
import { NON_NEGATIVE_INPUT_PROPS, sanitizeNumberInput } from "@/shared/lib";
|
||||
import { SectionCollapsible } from "@/shared/ui";
|
||||
import { Field } from "@/shared/ui/field";
|
||||
import { useAcntgSeMngTree } from "@/entities/acntg";
|
||||
@@ -59,7 +59,7 @@ function FnrsCstFormSection() {
|
||||
);
|
||||
|
||||
function handleChange(planStpNo: number, fieldKey: EditableFnrsKey, value: string) {
|
||||
const numValue = Number(stripMinusSign(value).replace(/,/g, "")) || 0;
|
||||
const numValue = Number(sanitizeNumberInput(value).replace(/,/g, "")) || 0;
|
||||
const draft = [...getValues("fnrsCstList")];
|
||||
const index = draft.findIndex((item) => item.planStpNo === planStpNo);
|
||||
if (index < 0) return;
|
||||
@@ -85,7 +85,7 @@ function FnrsCstFormSection() {
|
||||
<h3 className="text-xl font-semibold">세부별 총사업비(①) - 재원분담 총사업비(②)</h3>
|
||||
<span className="font-normal text-muted-foreground">(단위: 백만원)</span>
|
||||
</div>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<Table caption="총사업비 대비 재원분담 합계 비교" variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="22%" />
|
||||
@@ -107,7 +107,7 @@ function FnrsCstFormSection() {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>총사업비</TableHead>
|
||||
<TableHead scope="row">총사업비</TableHead>
|
||||
{remainingList.map((item) => (
|
||||
<TableCell key={item.planStpNo}>
|
||||
<div className={cn("text-right", item.remaining !== 0 ? "text-destructive" : "")}>
|
||||
@@ -125,7 +125,7 @@ function FnrsCstFormSection() {
|
||||
<h3 className="text-xl font-semibold">재원분담</h3>
|
||||
<span className="font-normal text-muted-foreground">(단위: 백만원)</span>
|
||||
</div>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<Table caption="단계별 재원분담 입력" variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="22%" />
|
||||
@@ -148,7 +148,7 @@ function FnrsCstFormSection() {
|
||||
<TableBody>
|
||||
{FNRS_CST_ROWS.map((row) => (
|
||||
<TableRow key={row.key}>
|
||||
<TableHead>{row.label}</TableHead>
|
||||
<TableHead scope="row">{row.label}</TableHead>
|
||||
{PLAN_STEPS.map((planStpNo) => {
|
||||
const item = fnrsCstList.find((cst) => cst.planStpNo === planStpNo);
|
||||
return (
|
||||
|
||||
@@ -79,7 +79,7 @@ function FnrsCstSection({ fnrsCstList, dtl }: FnrsCstSectionProps) {
|
||||
<h3 className="text-xl font-semibold">세부별 총사업비(①) - 재원분담 총사업비(②)</h3>
|
||||
<span className="font-normal text-muted-foreground">(단위: 백만원)</span>
|
||||
</div>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<Table caption="단계별 재원분담 내역" variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="22%" />
|
||||
@@ -102,7 +102,7 @@ function FnrsCstSection({ fnrsCstList, dtl }: FnrsCstSectionProps) {
|
||||
<TableBody>
|
||||
{FNRS_CST_ROWS.map((row) => (
|
||||
<TableRow key={row.key}>
|
||||
<TableHead>{row.label}</TableHead>
|
||||
<TableHead scope="row">{row.label}</TableHead>
|
||||
{stepHeaders.map((step) => {
|
||||
const item = matrixFnrsList.find((fnrs) => fnrs.planStpNo === step.planStpNo);
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState } from "react";
|
||||
import { useFormContext, useWatch } from "react-hook-form";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { NON_NEGATIVE_INPUT_PROPS, stripMinusSign } from "@/shared/lib";
|
||||
import { NON_NEGATIVE_INPUT_PROPS, sanitizeNumberInput } from "@/shared/lib";
|
||||
import { SectionCollapsible } from "@/shared/ui";
|
||||
import { Field } from "@/shared/ui/field";
|
||||
import { truncateDecimals } from "@/widgets/pbadms-sprt";
|
||||
@@ -62,8 +62,8 @@ function OutlineFormSection() {
|
||||
value: string,
|
||||
decimals?: number,
|
||||
) {
|
||||
const unsigned = stripMinusSign(value);
|
||||
const normalized = decimals === undefined ? unsigned : truncateDecimals(unsigned, decimals);
|
||||
const sanitized = sanitizeNumberInput(value);
|
||||
const normalized = decimals === undefined ? sanitized : truncateDecimals(sanitized, decimals);
|
||||
const numValue = Number(normalized.replace(/,/g, "")) || 0;
|
||||
const draft = [...getValues("sewerBscPlanOtlnList")];
|
||||
const index = draft.findIndex((item) => item.planStpNo === planStpNo);
|
||||
@@ -79,7 +79,7 @@ function OutlineFormSection() {
|
||||
이전기본계획조회
|
||||
</Button>
|
||||
</div>
|
||||
<Table variant="secondary">
|
||||
<Table caption="사업개요 단계별 입력" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="22%" />
|
||||
@@ -102,7 +102,7 @@ function OutlineFormSection() {
|
||||
<TableBody>
|
||||
{OTLN_ROWS.map((row) => (
|
||||
<TableRow key={row.key}>
|
||||
<TableHead>{row.label}</TableHead>
|
||||
<TableHead scope="row">{row.label}</TableHead>
|
||||
{[1, 2, 3, 4].map((planStpNo) => {
|
||||
const item = otlnList.find((otln) => otln.planStpNo === planStpNo);
|
||||
return (
|
||||
|
||||
@@ -29,7 +29,7 @@ function OutlineSection({ otlnList, dtl }: OutlineSectionProps) {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="사업개요">
|
||||
<Table variant="secondary">
|
||||
<Table caption="사업개요 단계별 현황" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="22%" />
|
||||
@@ -52,7 +52,7 @@ function OutlineSection({ otlnList, dtl }: OutlineSectionProps) {
|
||||
<TableBody>
|
||||
{OTLN_ROWS.map((row) => (
|
||||
<TableRow key={row.key}>
|
||||
<TableHead>{row.label}</TableHead>
|
||||
<TableHead scope="row">{row.label}</TableHead>
|
||||
{stepHeaders.map((step) => {
|
||||
const item = otlnList.find((otln) => otln.planStpNo === step.planStpNo);
|
||||
return (
|
||||
|
||||
@@ -187,7 +187,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||
<Table variant="secondary">
|
||||
<Table caption="검토의견 입력" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[12rem]">의견내용</TableHead>
|
||||
@@ -308,7 +308,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<Table caption="검토의견 목록: 의견자, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="55%" />
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
getDefaultFileKey,
|
||||
NON_NEGATIVE_INPUT_PROPS,
|
||||
stripMinusSign,
|
||||
sanitizeNumberInput,
|
||||
useTus,
|
||||
} from "@/shared/lib";
|
||||
import { TextShimmer } from "@/shared/ui";
|
||||
@@ -107,7 +107,7 @@ function TmplntDtlInput({ facilityIndex, planStpNo, fieldKey, decimals }: Tmplnt
|
||||
className="text-right"
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) => {
|
||||
const value = stripMinusSign(e.target.value.replace(/,/g, ""));
|
||||
const value = sanitizeNumberInput(e.target.value.replace(/,/g, ""));
|
||||
const normalized = hasDecimals ? truncateDecimals(value, decimals) : value;
|
||||
field.onChange(Number(normalized) || 0);
|
||||
}}
|
||||
@@ -147,7 +147,7 @@ function TmplntFacilityMatrix({
|
||||
const originalData = Array.isArray(data) ? (data[0] ?? null) : null;
|
||||
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<Table caption="처리시설별 단계별 계획 입력" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="9.75%" />
|
||||
@@ -199,7 +199,7 @@ function TmplntFacilityMatrix({
|
||||
<TableBody>
|
||||
{TMPLNT_FIELD_ROWS.map((row) => (
|
||||
<TableRow key={row.key}>
|
||||
<TableHead>{row.label}</TableHead>
|
||||
<TableHead scope="row">{row.label}</TableHead>
|
||||
{PLAN_STP_NOS.map((planStpNo) => (
|
||||
<TableCell key={`original-${row.key}-${planStpNo}`} className="text-right">
|
||||
{getOriginalValue(originalData, row.key, planStpNo)}
|
||||
@@ -317,7 +317,7 @@ function TmplntFacilityAttachments({
|
||||
return (
|
||||
<div className="mt-6 space-y-2">
|
||||
<h3 className="mb-4 text-xl font-semibold">처리구역 필수 첨부파일</h3>
|
||||
<Table variant="secondary">
|
||||
<Table caption="처리구역 필수 첨부파일" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="40%" />
|
||||
<col width="15%" />
|
||||
@@ -332,7 +332,7 @@ function TmplntFacilityAttachments({
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead data-required align="left">
|
||||
<TableHead scope="row" data-required align="left">
|
||||
{facilityName} 계획평면도
|
||||
</TableHead>
|
||||
<TableCell className="text-center">{TMPLNT_ATCH_FORMAT_LABEL}</TableCell>
|
||||
|
||||
@@ -102,7 +102,7 @@ function TmplntFacilityMatrix({
|
||||
const originalData = Array.isArray(data) ? (data[0] ?? null) : null;
|
||||
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<Table caption="처리시설별 단계별 계획 현황" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="9.75%" />
|
||||
@@ -154,7 +154,7 @@ function TmplntFacilityMatrix({
|
||||
<TableBody>
|
||||
{TMPLNT_FIELD_ROWS.map((row) => (
|
||||
<TableRow key={row.key}>
|
||||
<TableHead>{row.label}</TableHead>
|
||||
<TableHead scope="row">{row.label}</TableHead>
|
||||
{PLAN_STP_NOS.map((planStpNo) => (
|
||||
<TableCell key={`original-${row.key}-${planStpNo}`} className="text-right">
|
||||
{getOriginalValue(originalData, row.key, planStpNo)}
|
||||
@@ -242,7 +242,7 @@ function TmplntSection({ tmplntList, det, dtl }: TmplntSectionProps) {
|
||||
{(item.atchFileGroupId || item.atchFileInfo?.orgnlFileNm) && (
|
||||
<div className="mt-6 space-y-2">
|
||||
<h3 className="mb-4 text-xl font-semibold">처리구역 필수 첨부파일</h3>
|
||||
<Table variant="secondary">
|
||||
<Table caption="처리구역 첨부파일" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="35%" />
|
||||
<col width="65%" />
|
||||
|
||||
@@ -117,7 +117,7 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
|
||||
{isPending ? (
|
||||
<Skeleton className="h-[16rem] w-full" />
|
||||
) : (
|
||||
<Table variant="secondary">
|
||||
<Table caption="기본계획 신청 정보 입력" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
@@ -127,7 +127,7 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
|
||||
<TableHeader>
|
||||
{canSidoGugunSelectable && (
|
||||
<TableRow>
|
||||
<TableHead data-required>시도</TableHead>
|
||||
<TableHead scope="row" data-required>시도</TableHead>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={sidoComCd || undefined}
|
||||
@@ -148,7 +148,7 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableHead>시군구</TableHead>
|
||||
<TableHead scope="row">시군구</TableHead>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={sggComCd || undefined}
|
||||
@@ -170,7 +170,7 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableHead data-required>계획구분</TableHead>
|
||||
<TableHead scope="row" data-required>계획구분</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<FieldSet className="px-2">
|
||||
<RadioGroup
|
||||
@@ -199,7 +199,7 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>업무구분</TableHead>
|
||||
<TableHead scope="row" data-required>업무구분</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<FieldSet className="px-2">
|
||||
<RadioGroup value={taskSeComCd || undefined} onValueChange={setTaskSeComCd}>
|
||||
|
||||
@@ -246,8 +246,11 @@ function SewerBscPlanFormPage({
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSave)} className="mt-4">
|
||||
<SectionCollapsibleGroup>
|
||||
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
|
||||
<div className="sticky top-[6rem] z-30 flex items-center justify-between bg-container px-4 py-2">
|
||||
<SectionCollapsibleToggle />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<span className="text-red-600">*</span>은 필수입력 항목입니다.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{initialData?.detDto?.sewerBscPlanDocNo && (
|
||||
|
||||
@@ -63,6 +63,7 @@ function SewerBscPlanList({ searchParams, pagination, onPaginationChange }: Sewe
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "하수도 기본계획 목록",
|
||||
viewPagination: true,
|
||||
onRowClick: handleRowClick,
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user