Compare commits

...
91 Commits
Author SHA1 Message Date
jcchoi c2396f093b 필수입력 안내 메세지
Deploy Frontend / deploy (push) Successful in 8m31s
2026-10-07 19:43:06 +09:00
jcchoi bdf65dcfd1 설치인가 사업구분 팝업 사이즈 조정 2026-10-07 19:42:42 +09:00
jcchoi 2aefbe17ee 웹접근성 2차 수정 2026-10-07 19:42:20 +09:00
jcchoi 4482b92ccd 메뉴간 탭이동
Deploy Frontend / deploy (push) Successful in 7m33s
2026-10-06 17:41:39 +09:00
jcchoi d3c39caefb 웹 접근성 조치 2026-10-06 14:32:26 +09:00
jcchoi 24c5dce24d 대체 텍스트 적용 2026-10-06 13:18:58 +09:00
jcchoi d4d9eaf773 CM 로그인
Deploy Frontend / deploy (push) Successful in 5m32s
하단 메뉴 링크
2026-10-06 11:13:46 +09:00
jcchoi 68a800cdfc 메인페이지 탭이동
Deploy Frontend / deploy (push) Successful in 2m54s
2026-10-02 21:52:10 +09:00
jcchoi ff45d1a875 스킵 네비게이션 2026-10-02 21:51:56 +09:00
jcchoi 49000c6140 FAQ 웹접근성(툴팁) 2026-10-02 21:25:44 +09:00
jcchoi 24bba484ca Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m49s
2026-10-02 19:43:52 +09:00
jcchoi c8d0d85937 Merge branch 'deploy' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into deploy 2026-10-02 19:42:49 +09:00
chanii8 ad1e517472 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m56s
2026-10-02 17:55:30 +09:00
chanii8 17ed5eb46b Merge branch 'iipbs/as/dev' 2026-10-02 17:54:53 +09:00
chanii8 1a2faf02da [as] feat: 알림톡발송 페이지 구현 2026-10-02 17:54:25 +09:00
chanii8 d6522338aa [as] chore: 행정지원 숫자 폼 최대자릿수 제한 2026-10-02 16:37:26 +09:00
chanii8 df154a45b6 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m3s
2026-10-02 15:16:46 +09:00
chanii8 d438755616 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 7m37s
2026-10-02 13:12:44 +09:00
chanii8 4ee8d5a099 Merge branch 'deploy' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into deploy 2026-10-02 13:05:57 +09:00
jcchoi 538f656640 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Canceled after 6m27s
2026-10-01 22:25:22 +09:00
chanii8 8df79fd624 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m47s
2026-09-29 14:40:09 +09:00
chanii8 730be25948 Merge remote-tracking branch 'origin/main' into deploy
Deploy Frontend / deploy (push) Successful in 5m4s
2026-09-28 17:17:08 +09:00
chanii8 9c38165afb Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 7m25s
2026-09-28 11:24:12 +09:00
chanii8 9242e1fb47 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 12m36s
2026-09-23 14:44:23 +09:00
chanii8 271b5d445d [as] chore: 재원협의 버튼 수정
Deploy Frontend / deploy (push) Successful in 3m57s
2026-09-23 11:12:26 +09:00
chanii8 2457d14ae9 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 6m32s
2026-09-23 09:51:39 +09:00
chanii8 9739609de4 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m15s
2026-09-21 17:51:47 +09:00
chanii8 c799c199c5 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 8m28s
2026-09-21 16:58:49 +09:00
chanii8 5b3aaa463d Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m27s
2026-09-21 11:03:48 +09:00
chanii8 947dba5934 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m43s
2026-09-18 17:38:37 +09:00
chanii8 4816a512bf Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m40s
2026-09-17 17:08:09 +09:00
chanii8 4fad163b14 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m4s
2026-09-14 17:56:23 +09:00
chanii8 da2191f365 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m43s
2026-09-11 10:38:12 +09:00
chanii8 55ab2b8562 Merge remote-tracking branch 'origin/main' into deploy
Deploy Frontend / deploy (push) Successful in 3m33s
2026-09-10 17:53:02 +09:00
chanii8 526392c370 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m39s
2026-09-10 15:44:08 +09:00
chanii8 6020c80a7a Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 10m6s
2026-09-09 18:09:33 +09:00
chanii8 32b778f961 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m47s
2026-09-08 17:14:51 +09:00
chanii8 3946fda013 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m42s
2026-09-07 17:27:45 +09:00
chanii8 db30b45267 Merge branch 'deploy' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into deploy 2026-09-07 17:27:38 +09:00
jcchoi 6cc2a02df4 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 1m9s
2026-09-03 18:31:48 +09:00
chanii8 0f107ff29d Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 6m16s
2026-09-02 16:32:42 +09:00
chanii8 5e69a9b6f0 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m44s
2026-09-01 17:20:52 +09:00
chanii8 ec9311c76b Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m36s
2026-08-31 17:23:27 +09:00
chanii8 4c85a7d249 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m37s
2026-08-28 17:45:20 +09:00
chanii8 09d4c2d26e Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m6s
2026-08-28 17:13:45 +09:00
chanii8 b8dac5dc72 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m47s
2026-08-28 17:08:08 +09:00
chanii8 73cafc9786 Merge remote-tracking branch 'origin/main' into deploy
Deploy Frontend / deploy (push) Successful in 3m40s
2026-08-28 14:34:02 +09:00
chanii8 c276ae0c5e Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m2s
2026-08-27 17:25:06 +09:00
chanii8 b4ed061a53 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m40s
2026-08-27 11:36:49 +09:00
chanii8 ddaaa8fc92 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m55s
2026-08-26 17:14:14 +09:00
chanii8 3a253df58a Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 7m10s
2026-08-25 17:11:23 +09:00
chanii8 21debe2f77 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m22s
2026-08-25 15:45:10 +09:00
chanii8 0e4f662dfd Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m19s
2026-08-25 10:34:34 +09:00
chanii8 4a797808c1 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m1s
2026-08-24 17:18:45 +09:00
chanii8 3a81b822f2 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m54s
2026-08-21 17:29:09 +09:00
chanii8 f5a7efb324 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m2s
2026-08-20 13:12:39 +09:00
chanii8 abc3ca184d Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m49s
2026-08-18 18:03:01 +09:00
chanii8 267c022d58 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m36s
2026-08-13 17:52:39 +09:00
chanii8 0fc2844928 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m56s
2026-08-13 17:22:37 +09:00
chanii8 7f1fe42ace Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into deploy
Deploy Frontend / deploy (push) Successful in 3m36s
2026-08-11 17:46:50 +09:00
chanii8 c83a13d680 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m7s
2026-08-10 18:06:03 +09:00
chanii8 3d7dcb6d0e Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m17s
2026-08-07 17:03:11 +09:00
chanii8 a46f24d6d7 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m44s
2026-08-06 17:12:47 +09:00
chanii8 2e4f2428f0 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m41s
2026-08-06 16:50:33 +09:00
chanii8 706cebfeae Merge branch 'main' into deploy 2026-08-06 16:22:09 +09:00
chanii8 ee7f5e3a07 Merge branch 'main' into deploy 2026-08-06 13:42:33 +09:00
chanii8 3d0160f21f Merge branch 'main' into deploy 2026-08-05 17:23:37 +09:00
chanii8 0aa0126caf Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 5m43s
2026-08-03 17:47:39 +09:00
chanii8 5ecdd37187 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m52s
2026-08-03 17:28:33 +09:00
chanii8 20cc2ca248 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m48s
2026-08-03 11:45:43 +09:00
chanii8 5cfd7d65ec Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 7m45s
2026-07-31 18:00:11 +09:00
chanii8 3b8307b674 Merge branch 'main' into deploy 2026-07-30 11:37:16 +09:00
chanii8 2f800c960d Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 10m24s
2026-07-24 16:59:38 +09:00
chanii8 2728b4058b Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m8s
2026-07-24 10:57:06 +09:00
chanii8 b842905572 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m28s
2026-07-23 11:44:35 +09:00
chanii8 85fa9a8658 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into deploy
Deploy Frontend / deploy (push) Successful in 10m37s
2026-07-22 14:42:42 +09:00
chanii8 5ae56481c2 Merge remote-tracking branch 'origin/main' into deploy
Deploy Frontend / deploy (push) Successful in 7m23s
2026-07-22 11:44:50 +09:00
chanii8 8079d7d6af Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 7m20s
2026-07-21 11:51:55 +09:00
chanii8 7c8eef1715 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 6m51s
2026-07-20 18:44:05 +09:00
chanii8 f10b3179c2 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 10m25s
2026-07-20 17:11:46 +09:00
chanii8 2e0bb5aa84 Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into deploy
Deploy Frontend / deploy (push) Successful in 10m10s
2026-07-20 13:52:10 +09:00
chanii8 7ae8d42731 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 9m2s
2026-07-16 18:39:59 +09:00
chanii8 a9e65af77b Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 3m25s
2026-07-13 11:14:34 +09:00
chanii8 d21ecc975f Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 2m14s
2026-07-09 17:54:09 +09:00
chanii8 452509281a Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 5m8s
2026-07-09 17:40:25 +09:00
chanii8 2e3602eee6 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 11m12s
2026-07-08 18:55:17 +09:00
jcchoi 7dfd535841 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 2m54s
2026-07-06 16:54:57 +09:00
jcchoi 39d928b5b1 Merge remote-tracking branch 'origin/iipbs/st/dev' into deploy 2026-07-06 16:51:29 +09:00
jcchoi 525695072a Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 2m56s
2026-07-05 11:07:27 +09:00
jcchoi 972a4ce789 Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 5m30s
2026-07-02 18:34:53 +09:00
jcchoi 879aadc068 Merge remote-tracking branch 'origin/iipbs/st/dev' into deploy
Deploy Frontend / deploy (push) Successful in 3m2s
2026-07-02 18:29:55 +09:00
150 changed files with 1881 additions and 410 deletions
@@ -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;
+15 -1
View File
@@ -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,
});
}
+2 -1
View File
@@ -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),
});
}
+14
View File
@@ -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>;
}
+8 -2
View File
@@ -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,
},
});
+1
View File
@@ -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