[as] WIP: 설치인가 버튼 로직
[as] WIP: 설치인가 버튼 로직 [as] WIP: 설치인가 버튼 로직
This commit is contained in:
@@ -4,11 +4,14 @@ import type { PageResponse } from "@/shared/model";
|
||||
|
||||
import type {
|
||||
AdstZoneResponseDto,
|
||||
DocPrgrsUpdateRequestDto,
|
||||
InstlSncnApplySaveRequestDto,
|
||||
InstlSncnMngDetResponse,
|
||||
InstlSncnMngResponseDto,
|
||||
InstlSncnMngSaveRequestDto,
|
||||
InstlSncnMngSearchParams,
|
||||
InstlSncnMngService,
|
||||
InstlSncnMngUpdateRequestDto,
|
||||
} from "../model/instl-sncn-interface";
|
||||
|
||||
async function getSidoCombo(): Promise<AdstZoneResponseDto[]> {
|
||||
@@ -30,32 +33,58 @@ async function getSigunguCombo(sidoCd: string): Promise<AdstZoneResponseDto[]> {
|
||||
|
||||
const BASE = ENDPOINT.AS_SERVICE.INSTL_SNCN;
|
||||
|
||||
export class InstlSncnApiService implements InstlSncnMngService {
|
||||
public async getPage(
|
||||
params: InstlSncnMngSearchParams,
|
||||
): Promise<PageResponse<InstlSncnMngResponseDto>> {
|
||||
const response = await client.get<PageResponse<InstlSncnMngResponseDto>>(BASE, {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getById(id: string): Promise<InstlSncnMngDetResponse> {
|
||||
const response = await client.get<InstlSncnMngDetResponse>(`${BASE}/${id}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async saveApply(payload: InstlSncnApplySaveRequestDto): Promise<string> {
|
||||
const response = await client.post<string>(`${BASE}/save-apply`, payload);
|
||||
return response.data;
|
||||
}
|
||||
public async techReview(id: string, payload: InstlSncnApplySaveRequestDto): Promise<void> {
|
||||
await client.post(`${BASE}/${id}/tech-review`, payload);
|
||||
}
|
||||
async function getPage(
|
||||
params: InstlSncnMngSearchParams,
|
||||
): Promise<PageResponse<InstlSncnMngResponseDto>> {
|
||||
const response = await client.get<PageResponse<InstlSncnMngResponseDto>>(BASE, {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const InstlSncnApi = new InstlSncnApiService();
|
||||
async function getById(id: string): Promise<InstlSncnMngDetResponse> {
|
||||
const response = await client.get<InstlSncnMngDetResponse>(`${BASE}/${id}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function save(payload: InstlSncnMngSaveRequestDto): Promise<string> {
|
||||
const response = await client.post<string>(BASE, payload);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function saveApply(payload: InstlSncnApplySaveRequestDto): Promise<string> {
|
||||
const response = await client.post<string>(`${BASE}/save-apply`, payload);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function update(id: string, payload: InstlSncnMngUpdateRequestDto): Promise<void> {
|
||||
await client.put(`${BASE}/${id}`, payload);
|
||||
}
|
||||
|
||||
async function techReview(id: string, payload: InstlSncnApplySaveRequestDto): Promise<void> {
|
||||
await client.post(`${BASE}/${id}/tech-review`, payload);
|
||||
}
|
||||
|
||||
async function remove(id: string): Promise<void> {
|
||||
await client.delete(`${BASE}/${id}`);
|
||||
}
|
||||
|
||||
async function changePrgrsSttsComCd(payload: DocPrgrsUpdateRequestDto): Promise<void> {
|
||||
await client.post(`${BASE}/doc/updatePrgrsStts`, payload);
|
||||
}
|
||||
|
||||
export const instlSncnService = {
|
||||
getPage,
|
||||
getById,
|
||||
save,
|
||||
saveApply,
|
||||
update,
|
||||
techReview,
|
||||
remove,
|
||||
changePrgrsSttsComCd,
|
||||
} satisfies InstlSncnMngService;
|
||||
|
||||
// TODO: st-anls-mst service 로 분리
|
||||
export const instlSncnRegionService = {
|
||||
getSidoCombo,
|
||||
getSigunguCombo,
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type { InstlSncnMngSearchParams } from "../model/instl-sncn-interface";
|
||||
import type {
|
||||
DocPrgrsUpdateRequestDto,
|
||||
InstlSncnApplySaveRequestDto,
|
||||
InstlSncnMngSaveRequestDto,
|
||||
InstlSncnMngSearchParams,
|
||||
InstlSncnMngUpdateRequestDto,
|
||||
} from "../model/instl-sncn-interface";
|
||||
|
||||
import { InstlSncnApi, instlSncnRegionService } from "./instl-sncn-service";
|
||||
import { instlSncnRegionService, instlSncnService } from "./instl-sncn-service";
|
||||
|
||||
export const instlSncnKeys = {
|
||||
all: ["instl-sncn"] as const,
|
||||
list: (params: InstlSncnMngSearchParams) => [...instlSncnKeys.all, "list", params] as const,
|
||||
list: () => [...instlSncnKeys.all, "list"] as const,
|
||||
detail: (id: string) => [...instlSncnKeys.all, "detail", id] as const,
|
||||
sidoCombo: () => [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST, "sido"] as const,
|
||||
sigunguCombo: (sidoCd: string) =>
|
||||
@@ -17,15 +23,15 @@ export const instlSncnKeys = {
|
||||
|
||||
export function useInstlSncnList(params: InstlSncnMngSearchParams) {
|
||||
return useQuery({
|
||||
queryKey: instlSncnKeys.list(params),
|
||||
queryFn: () => InstlSncnApi.getPage(params),
|
||||
queryKey: instlSncnKeys.list(),
|
||||
queryFn: () => instlSncnService.getPage(params),
|
||||
});
|
||||
}
|
||||
|
||||
export function useInstlSncnDetail(id: string) {
|
||||
return useQuery({
|
||||
queryKey: instlSncnKeys.detail(id),
|
||||
queryFn: () => InstlSncnApi.getById(id),
|
||||
queryFn: () => instlSncnService.getById(id),
|
||||
enabled: Boolean(id),
|
||||
});
|
||||
}
|
||||
@@ -44,3 +50,77 @@ export function useInstlSncnSigunguCombo(sidoCd: string) {
|
||||
enabled: Boolean(sidoCd),
|
||||
});
|
||||
}
|
||||
|
||||
export function useInstlSncnSave() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (payload: InstlSncnMngSaveRequestDto) => instlSncnService.save(payload),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useInstlSncnSaveApply() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (payload: InstlSncnApplySaveRequestDto) => instlSncnService.saveApply(payload),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useInstlSncnUpdate() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, payload }: { id: string; payload: InstlSncnMngUpdateRequestDto }) =>
|
||||
instlSncnService.update(id, payload),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
|
||||
queryClient.invalidateQueries({ queryKey: instlSncnKeys.detail(variables.id) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useInstlSncnTechReview() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, payload }: { id: string; payload: InstlSncnApplySaveRequestDto }) =>
|
||||
instlSncnService.techReview(id, payload),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
|
||||
queryClient.invalidateQueries({ queryKey: instlSncnKeys.detail(variables.id) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useInstlSncnRemove() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => instlSncnService.remove(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useInstlSncnChangePrgrsStts() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (payload: DocPrgrsUpdateRequestDto) =>
|
||||
instlSncnService.changePrgrsSttsComCd(payload),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: instlSncnKeys.detail(variables.docNo),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
export { InstlSncnApi, instlSncnRegionService } from "./api/instl-sncn-service";
|
||||
export { instlSncnRegionService, instlSncnService } from "./api/instl-sncn-service";
|
||||
export {
|
||||
instlSncnKeys,
|
||||
useInstlSncnDetail,
|
||||
useInstlSncnList,
|
||||
useInstlSncnSidoCombo,
|
||||
useInstlSncnSigunguCombo,
|
||||
useInstlSncnTechReview,
|
||||
} from "./api/queries";
|
||||
export {
|
||||
type AdstZoneResponseDto,
|
||||
@@ -12,6 +13,7 @@ export {
|
||||
type BizCstTreeDto,
|
||||
type BizSclTreeDto,
|
||||
type CklstRvwRsltTreeDto,
|
||||
type DocPrgrsUpdateRequestDto,
|
||||
type ElaprDocPrgrsClientResponseDto,
|
||||
type EsntlAtchFileMngTreeResponseDto,
|
||||
INSTL_SNCN_REGISTER_SELECTION_KEY,
|
||||
@@ -19,10 +21,13 @@ export {
|
||||
type InstlSncnBizCstSaveRequestDto,
|
||||
type InstlSncnBzScaleSaveDto,
|
||||
type InstlSncnCklstResultSaveDto,
|
||||
type InstlSncnEsntlAtchFileSaveDto,
|
||||
type InstlSncnMngDetResponse,
|
||||
type InstlSncnMngResponseDto,
|
||||
type InstlSncnMngSaveRequestDto,
|
||||
type InstlSncnMngSearchParams,
|
||||
type InstlSncnRvwOpnnResponseDto,
|
||||
type InstlSncnRvwOpnnSaveRequestDto,
|
||||
type InstlSncnSplmntDmndSaveDto,
|
||||
type ReviewStepCode,
|
||||
} from "./model/instl-sncn-interface";
|
||||
|
||||
@@ -67,6 +67,19 @@ export interface InstlSncnMngSaveRequestDto {
|
||||
rtlztnUsgSeComCd?: string;
|
||||
}
|
||||
|
||||
/** 설치인가관리 수정 요청 DTO */
|
||||
export type InstlSncnMngUpdateRequestDto = InstlSncnMngSaveRequestDto;
|
||||
|
||||
/** 문서 진행상태 수정 요청 DTO */
|
||||
export interface DocPrgrsUpdateRequestDto {
|
||||
/** 행정지원문서번호 */
|
||||
docNo: string;
|
||||
/** 문서구분코드 */
|
||||
docSeComCd: string;
|
||||
/** 진행상태공통코드 */
|
||||
prgrsSttsComCd: string;
|
||||
}
|
||||
|
||||
/** 설치인가관리 응답 DTO */
|
||||
export interface InstlSncnMngResponseDto extends InstlSncnMngSaveRequestDto {
|
||||
/** 설치인가문서번호 (필수로 재정의) */
|
||||
@@ -101,14 +114,14 @@ export interface InstlSncnMngResponseDto extends InstlSncnMngSaveRequestDto {
|
||||
export interface InstlSncnBzScaleSaveDto {
|
||||
/** 공종구분코드 */
|
||||
cnstpSeCd: string;
|
||||
/** 기본계획(A) */
|
||||
/** 기본계획수량 */
|
||||
bscPl: number;
|
||||
/** 예산반영(B) */
|
||||
/** 예산반영수량 */
|
||||
budgetReflct: number;
|
||||
/** 금회설계시(C) */
|
||||
/** 금회설계수량 */
|
||||
thtiDsgn: number;
|
||||
/** 증감(C-B) */
|
||||
diff: number;
|
||||
/** 증감(C-B) — 클라이언트 계산용, API 미전송 */
|
||||
diff?: number;
|
||||
}
|
||||
|
||||
/** 설치인가 신청 - 총사업비 저장 DTO(배열 원소) */
|
||||
@@ -125,27 +138,92 @@ export interface InstlSncnBizCstSaveRequestDto {
|
||||
|
||||
/** 설치인가 신청 - 체크리스트 결과 저장 DTO(배열 원소) */
|
||||
export interface InstlSncnCklstResultSaveDto {
|
||||
/** 점검표일련번호 */
|
||||
cklstSn: number;
|
||||
/** 점검표항목일련번호 */
|
||||
cklstArtclSn: number;
|
||||
/** 점검표검토일련번호 */
|
||||
cklstRvwSn: number;
|
||||
/** 검토단계공통코드 */
|
||||
rvwStpComCd: string;
|
||||
/** 선택값(Y/N/null) */
|
||||
selected: "Y" | "N" | null;
|
||||
/** 담당자검토내용(기술검토의견) */
|
||||
picRvwCn?: string | null;
|
||||
}
|
||||
|
||||
/** 설치인가 신청 - 필수첨부파일 저장 DTO(배열 원소) */
|
||||
export interface InstlSncnEsntlAtchFileSaveDto {
|
||||
/** 필수첨부파일아이디 */
|
||||
esntlAtchFileId: string;
|
||||
/** 첨부파일그룹아이디 */
|
||||
atchFileGroupId: string;
|
||||
}
|
||||
|
||||
/** 설치인가검토의견 저장 요청 DTO */
|
||||
export interface InstlSncnRvwOpnnSaveRequestDto {
|
||||
/** 설치인가문서번호 */
|
||||
instlSncnDocNo: string;
|
||||
/** 검토의견일련번호 */
|
||||
rvwOpnnSn?: number;
|
||||
/** 의견자구분공통코드 */
|
||||
opnrSeComCd?: string;
|
||||
/** 의견자명 */
|
||||
opnrNm?: string;
|
||||
/** 검토결과내용 */
|
||||
rvwRsltCn?: string;
|
||||
/** 검토결과이상여부 */
|
||||
rvwRsltAnmlyYn?: string;
|
||||
/** 첨부파일그룹아이디 */
|
||||
atchFileGroupId?: string;
|
||||
}
|
||||
|
||||
/** 설치인가 보완요구 저장 DTO(배열 원소) */
|
||||
export interface InstlSncnSplmntDmndSaveDto {
|
||||
/** 보완요구 일련번호 */
|
||||
splmntDmndSn?: number;
|
||||
/** 보완항목 공통코드 */
|
||||
splmntArtclComCd?: string;
|
||||
/** 보완요구 내용 */
|
||||
splmntDmndCn?: string;
|
||||
/** 보완요구 일시 */
|
||||
splmntDmndDt?: string;
|
||||
/** 보완요구 첨부파일 그룹 ID */
|
||||
splmntDmndAtchFileGroupId?: string;
|
||||
/** 보완 내용 */
|
||||
splmntCn?: string;
|
||||
/** 보완 담당자 명 */
|
||||
splmntPicNm?: string;
|
||||
/** 보완완료 일시 */
|
||||
splmntCmptnDt?: string;
|
||||
/** 보완 첨부파일 그룹 ID */
|
||||
splmntAtchFileGroupId?: string;
|
||||
/** 완료 여부 */
|
||||
cmptnYn?: string;
|
||||
}
|
||||
|
||||
/** 설치인가 신청 - 통합 저장 요청 DTO */
|
||||
export interface InstlSncnApplySaveRequestDto {
|
||||
/** 기술검토저장구분(SV.저장, CP.검토완료) */
|
||||
techReViewSaveCd?: string;
|
||||
/** 검토단계공통코드(ENV.환경청.EP, KECO.환경공단.TE) */
|
||||
rvwStpCd?: string;
|
||||
/** 설치인가관리 정보 */
|
||||
instlSncnMng: InstlSncnMngSaveRequestDto;
|
||||
/** 사업규모 목록 */
|
||||
bzScaleList: InstlSncnBzScaleSaveDto[];
|
||||
/** 총사업비 목록 */
|
||||
bizCstList: InstlSncnBizCstSaveRequestDto[];
|
||||
/** 체크리스트 검토결과 목록 */
|
||||
cklstResultList: InstlSncnCklstResultSaveDto[];
|
||||
/** 체크리스트 검토결과 맵 (rvwStpComCd -> 목록) */
|
||||
cklstResultMap?: Record<string, InstlSncnCklstResultSaveDto[]>;
|
||||
/** 필수첨부파일 목록 */
|
||||
esntlAtchFileList: InstlSncnEsntlAtchFileSaveDto[];
|
||||
/** 검토의견 목록 */
|
||||
rvwOpnnList?: InstlSncnRvwOpnnSaveRequestDto[];
|
||||
/** 보완요구 목록 */
|
||||
splmntDmndList?: InstlSncnSplmntDmndSaveDto[];
|
||||
}
|
||||
|
||||
export type ReviewStepCode = "LOCAL" | "ENV" | "KECO";
|
||||
@@ -334,6 +412,10 @@ export interface InstlSncnMngDetResponse {
|
||||
export interface InstlSncnMngService {
|
||||
getPage: (params: InstlSncnMngSearchParams) => Promise<PageResponse<InstlSncnMngResponseDto>>;
|
||||
getById: (id: string) => Promise<InstlSncnMngDetResponse>;
|
||||
save: (payload: InstlSncnMngSaveRequestDto) => Promise<string>;
|
||||
saveApply: (payload: InstlSncnApplySaveRequestDto) => Promise<string>;
|
||||
update: (id: string, payload: InstlSncnMngUpdateRequestDto) => Promise<void>;
|
||||
techReview: (id: string, payload: InstlSncnApplySaveRequestDto) => Promise<void>;
|
||||
remove: (id: string) => Promise<void>;
|
||||
changePrgrsSttsComCd: (payload: DocPrgrsUpdateRequestDto) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ function ElaprTmpltFormDialog({
|
||||
<div className="space-y-2">
|
||||
<Label>문서구분</Label>
|
||||
<CodeSelect
|
||||
upCd="ASDOCSECD"
|
||||
upCd="ASDOCSE"
|
||||
value={docSeComCd}
|
||||
onValueChange={(v) => setValue("docSeComCd", v)}
|
||||
placeholder="문서구분 선택"
|
||||
|
||||
@@ -67,6 +67,7 @@ function CklstPreviewDialog({
|
||||
<TableBody>
|
||||
{displayRows.map(({ row, depth }) => {
|
||||
const branchPrefix = formatTreeBranchPrefix(depth);
|
||||
const showRadios = row.bchckUseYn !== "N";
|
||||
|
||||
if (row.useYn === "N") return null;
|
||||
|
||||
@@ -82,16 +83,20 @@ function CklstPreviewDialog({
|
||||
</TableCell>
|
||||
<TableCell>{row.rvwCn ?? "—"}</TableCell>
|
||||
<TableCell align="center">
|
||||
<RadioGroup className="flex items-center justify-center">
|
||||
<div className="flex items-center gap-1">
|
||||
<RadioGroupItem value="Y" id={`${row.rowKey}-use-y`} />
|
||||
<Label htmlFor={`${row.rowKey}-use-y`}>예</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<RadioGroupItem value="N" id={`${row.rowKey}-use-n`} />
|
||||
<Label htmlFor={`${row.rowKey}-use-n`}>아니오</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
{showRadios ? (
|
||||
<RadioGroup className="flex items-center justify-center">
|
||||
<div className="flex items-center gap-1">
|
||||
<RadioGroupItem value="Y" id={`${row.rowKey}-preview-y`} />
|
||||
<Label htmlFor={`${row.rowKey}-preview-y`}>예</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<RadioGroupItem value="N" id={`${row.rowKey}-preview-n`} />
|
||||
<Label htmlFor={`${row.rowKey}-preview-n`}>아니오</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
@@ -113,6 +113,7 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) {
|
||||
rvwCnLvNo: 1,
|
||||
rvwCn: "",
|
||||
useYn: "Y",
|
||||
bchckUseYn: "Y",
|
||||
sortSeq: maxSort + 1,
|
||||
isNew: true,
|
||||
isDirty: true,
|
||||
@@ -145,6 +146,7 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) {
|
||||
rvwCnLvNo: parentLv + 1,
|
||||
rvwCn: "",
|
||||
useYn: "Y",
|
||||
bchckUseYn: "Y",
|
||||
sortSeq: maxSort + 1,
|
||||
isNew: true,
|
||||
isDirty: true,
|
||||
@@ -189,6 +191,8 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) {
|
||||
const handleSave = async () => {
|
||||
if (!cklstSn) return;
|
||||
|
||||
if (!(await confirm("체크리스트 항목을 저장하시겠습니까?"))) return;
|
||||
|
||||
const dirty = getDirtyRows().sort((a, b) => (a.rvwCnLvNo ?? 1) - (b.rvwCnLvNo ?? 1));
|
||||
|
||||
for (const row of dirty) {
|
||||
@@ -198,8 +202,6 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await confirm("체크리스트 항목을 저장하시겠습니까?"))) return;
|
||||
|
||||
if (row.isNew) {
|
||||
await createMutation.mutateAsync({
|
||||
cklstSn: row.cklstSn,
|
||||
@@ -209,7 +211,7 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) {
|
||||
rvwCn: row.rvwCn,
|
||||
useYn: row.useYn,
|
||||
sortSeq: row.sortSeq,
|
||||
bchckUseYn: "Y",
|
||||
bchckUseYn: row.bchckUseYn ?? "Y",
|
||||
});
|
||||
} else if (row.cklstRvwSn != null) {
|
||||
await updateMutation.mutateAsync({
|
||||
@@ -277,17 +279,19 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) {
|
||||
) : (
|
||||
<Table className="w-full">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="50%" />
|
||||
<col width="15%" />
|
||||
<col width="18%" />
|
||||
<col width="42%" />
|
||||
<col width="12%" />
|
||||
<col width="12%" />
|
||||
<col width="5%" />
|
||||
<col width="10%" />
|
||||
<col width="11%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>점검항목</TableHead>
|
||||
<TableHead>주요 검토내용</TableHead>
|
||||
<TableHead>사용 여부</TableHead>
|
||||
<TableHead>체크 사용</TableHead>
|
||||
<TableHead>구분</TableHead>
|
||||
<TableHead>관리</TableHead>
|
||||
</TableRow>
|
||||
|
||||
@@ -91,6 +91,22 @@ function CklstRvwRowComponent({
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<RadioGroup
|
||||
value={row.bchckUseYn ?? "Y"}
|
||||
onValueChange={(v) => onChange({ bchckUseYn: v as "Y" | "N", isDirty: true })}
|
||||
className="flex justify-center gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="Y" id={`${row.rowKey}-bchck-y`} />
|
||||
<Label htmlFor={`${row.rowKey}-bchck-y`}>예</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="N" id={`${row.rowKey}-bchck-n`} />
|
||||
<Label htmlFor={`${row.rowKey}-bchck-n`}>아니오</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</TableCell>
|
||||
<TableCell align="center">{levelLabel}</TableCell>
|
||||
<TableCell align="center">
|
||||
<div className="flex justify-center gap-2">
|
||||
|
||||
@@ -94,7 +94,7 @@ function EsntlAtchFileFormDialog({
|
||||
<div className="space-y-2">
|
||||
<Label>문서구분</Label>
|
||||
<CodeSelect
|
||||
upCd="ASDOCSECD"
|
||||
upCd="ASDOCSE"
|
||||
value={docSeComCd}
|
||||
onValueChange={(v) => setValue("docSeComCd", v, { shouldValidate: true })}
|
||||
placeholder="문서구분 선택"
|
||||
|
||||
@@ -36,7 +36,7 @@ function EsntlAtchFileList({
|
||||
onPaginationChange,
|
||||
}: EsntlAtchFileListProps) {
|
||||
const { confirm } = useConfirm();
|
||||
const { data: docSeCodes } = useCodeList("ASDOCSECD");
|
||||
const { data: docSeCodes } = useCodeList("ASDOCSE");
|
||||
|
||||
const [dialogMode, setDialogMode] = useState<"create" | "edit" | null>(null);
|
||||
const [editingRow, setEditingRow] = useState<EsntlAtchFileMng | null>(null);
|
||||
|
||||
@@ -32,7 +32,7 @@ const DEFAULT_SEARCH_PARAMS = {
|
||||
};
|
||||
|
||||
function EsntlAtchFileMngPage() {
|
||||
const { data: docSeCodes, isPending: isDocSeLoading } = useCodeList("ASDOCSECD");
|
||||
const { data: docSeCodes, isPending: isDocSeLoading } = useCodeList("ASDOCSE");
|
||||
|
||||
const [docSeComCd, setDocSeComCd] = useState(ESNTL_ATCH_FILE_ALL_DOC_SE);
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
|
||||
@@ -9,8 +9,8 @@ import { dayjs } from "@/shared/lib";
|
||||
import { useCodeList } from "@/entities/code";
|
||||
import {
|
||||
INSTL_SNCN_REGISTER_SELECTION_KEY,
|
||||
InstlSncnApi,
|
||||
type InstlSncnMngSearchParams,
|
||||
instlSncnService,
|
||||
} from "@/entities/instl-sncn";
|
||||
|
||||
import "./cnfm-page.css";
|
||||
@@ -78,14 +78,14 @@ function CnfmListPage() {
|
||||
useEffect(() => {
|
||||
if (!taskDialogOpen) return;
|
||||
if (selectedTaskSeComCd.length) return;
|
||||
if (fnrsCodeList.length === 0) return;
|
||||
// setSelectedTaskSeComCd([fnrsCodeList[0].lwpoCd]);
|
||||
if (fnrsCodeList.length === 0) return;
|
||||
// setSelectedTaskSeComCd([fnrsCodeList[0].lwpoCd]);
|
||||
}, [taskDialogOpen, selectedTaskSeComCd, fnrsCodeList]);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["instl-sncn-list", search, pageIndex, pageSize],
|
||||
queryFn: () =>
|
||||
InstlSncnApi.getPage({
|
||||
instlSncnService.getPage({
|
||||
...search,
|
||||
page: pageIndex,
|
||||
size: pageSize,
|
||||
@@ -120,13 +120,15 @@ function CnfmListPage() {
|
||||
|
||||
// prepare arrays of selected codes and names
|
||||
const selectedCodes = selectedTaskSeComCd;
|
||||
const selectedNames = selectedTaskSeComCd.map((code) => fnrsCodeList.find((item) => item.lwpoCd === code)?.lwpoCdNm || "");
|
||||
const selectedNames = selectedTaskSeComCd.map(
|
||||
(code) => fnrsCodeList.find((item) => item.lwpoCd === code)?.lwpoCdNm || "",
|
||||
);
|
||||
sessionStorage.setItem(
|
||||
INSTL_SNCN_REGISTER_SELECTION_KEY,
|
||||
JSON.stringify({
|
||||
taskSeComCdList: selectedCodes,
|
||||
taskSeComCdNmList: selectedNames
|
||||
})
|
||||
taskSeComCdNmList: selectedNames,
|
||||
}),
|
||||
);
|
||||
setTaskDialogOpen(false);
|
||||
router.push("/instl-sncn/register");
|
||||
@@ -138,7 +140,9 @@ function CnfmListPage() {
|
||||
<div className="cnfm-card">
|
||||
<form onSubmit={handleSearch} className="cnfm-search-bar">
|
||||
<div className="cnfm-search-field">
|
||||
<label className="cnfm-search-label" htmlFor="sidoComCd">시도</label>
|
||||
<label className="cnfm-search-label" htmlFor="sidoComCd">
|
||||
시도
|
||||
</label>
|
||||
<select
|
||||
id="sidoComCd"
|
||||
className="cnfm-select"
|
||||
@@ -146,13 +150,17 @@ function CnfmListPage() {
|
||||
onChange={(e) => setSidoComCd(e.target.value)}
|
||||
>
|
||||
{SIDO_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="cnfm-search-field">
|
||||
<label className="cnfm-search-label" htmlFor="sggComCd">시군구</label>
|
||||
<label className="cnfm-search-label" htmlFor="sggComCd">
|
||||
시군구
|
||||
</label>
|
||||
<select
|
||||
id="sggComCd"
|
||||
className="cnfm-select"
|
||||
@@ -164,7 +172,9 @@ function CnfmListPage() {
|
||||
</div>
|
||||
|
||||
<div className="cnfm-search-field cnfm-search-field--grow">
|
||||
<label className="cnfm-search-label" htmlFor="bizNm">사업명</label>
|
||||
<label className="cnfm-search-label" htmlFor="bizNm">
|
||||
사업명
|
||||
</label>
|
||||
<input
|
||||
id="bizNm"
|
||||
type="text"
|
||||
@@ -176,7 +186,9 @@ function CnfmListPage() {
|
||||
</div>
|
||||
|
||||
<div className="cnfm-search-field">
|
||||
<label className="cnfm-search-label" htmlFor="taskSeComCd">구분</label>
|
||||
<label className="cnfm-search-label" htmlFor="taskSeComCd">
|
||||
구분
|
||||
</label>
|
||||
<select
|
||||
id="taskSeComCd"
|
||||
className="cnfm-select cnfm-select--wide"
|
||||
@@ -184,12 +196,16 @@ function CnfmListPage() {
|
||||
onChange={(e) => setTaskSeComCd(e.target.value)}
|
||||
>
|
||||
{TASK_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" className="cnfm-btn-search">검색</button>
|
||||
<button type="submit" className="cnfm-btn-search">
|
||||
검색
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -198,11 +214,7 @@ function CnfmListPage() {
|
||||
<div className="cnfm-list-header">
|
||||
<h3 className="cnfm-list-title">설치인가 목록</h3>
|
||||
<div className="cnfm-list-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="cnfm-btn-register"
|
||||
onClick={handleOpenTaskDialog}
|
||||
>
|
||||
<button type="button" className="cnfm-btn-register" onClick={handleOpenTaskDialog}>
|
||||
✎ 신청
|
||||
</button>
|
||||
<select
|
||||
@@ -214,7 +226,9 @@ function CnfmListPage() {
|
||||
}}
|
||||
>
|
||||
{[10, 20, 30, 50].map((size) => (
|
||||
<option key={size} value={size}>{size}개</option>
|
||||
<option key={size} value={size}>
|
||||
{size}개
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
@@ -237,12 +251,16 @@ function CnfmListPage() {
|
||||
<tbody>
|
||||
{query.isLoading && (
|
||||
<tr>
|
||||
<td colSpan={8} className="cnfm-td-empty">조회 중입니다...</td>
|
||||
<td colSpan={8} className="cnfm-td-empty">
|
||||
조회 중입니다...
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!query.isLoading && (query.data?.content?.length ?? 0) === 0 && (
|
||||
<tr>
|
||||
<td colSpan={8} className="cnfm-td-empty">조회 결과가 없습니다.</td>
|
||||
<td colSpan={8} className="cnfm-td-empty">
|
||||
조회 결과가 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{(query.data?.content ?? []).map((row) => {
|
||||
@@ -254,7 +272,7 @@ function CnfmListPage() {
|
||||
onClick={() => router.push(`/instl-sncn/${row.instlSncnDocNo}`)}
|
||||
>
|
||||
<td>{row.sido || "-"}</td>
|
||||
<td>{row.gugun|| "-"}</td>
|
||||
<td>{row.gugun || "-"}</td>
|
||||
<td className="text-left">{row.bizNm || "-"}</td>
|
||||
<td>{row.taskSeCdNm || row.taskSeComCd || "-"}</td>
|
||||
<td>{row.aplcntNm || "-"}</td>
|
||||
@@ -267,10 +285,13 @@ function CnfmListPage() {
|
||||
index < currentStepIndex
|
||||
? "completed"
|
||||
: index === currentStepIndex
|
||||
? "current"
|
||||
: "pending";
|
||||
? "current"
|
||||
: "pending";
|
||||
return (
|
||||
<span key={step.code} className={`cnfm-step-badge cnfm-step-badge--${state}`}>
|
||||
<span
|
||||
key={step.code}
|
||||
className={`cnfm-step-badge cnfm-step-badge--${state}`}
|
||||
>
|
||||
<span className={`cnfm-step-dot cnfm-step-dot--${state}`} />
|
||||
{step.label}
|
||||
</span>
|
||||
@@ -284,10 +305,13 @@ function CnfmListPage() {
|
||||
index < currentStepIndex
|
||||
? "completed"
|
||||
: index === currentStepIndex
|
||||
? "current"
|
||||
: "pending";
|
||||
? "current"
|
||||
: "pending";
|
||||
return (
|
||||
<span key={step.code} className={`cnfm-step-badge cnfm-step-badge--${state}`}>
|
||||
<span
|
||||
key={step.code}
|
||||
className={`cnfm-step-badge cnfm-step-badge--${state}`}
|
||||
>
|
||||
<span className={`cnfm-step-dot cnfm-step-dot--${state}`} />
|
||||
{step.label}
|
||||
</span>
|
||||
@@ -378,7 +402,9 @@ function CnfmListPage() {
|
||||
checked={selectedTaskSeComCd.includes(item.lwpoCd)}
|
||||
onChange={() => {
|
||||
setSelectedTaskSeComCd((prev) =>
|
||||
prev.includes(item.lwpoCd) ? prev.filter((v) => v !== item.lwpoCd) : [...prev, item.lwpoCd],
|
||||
prev.includes(item.lwpoCd)
|
||||
? prev.filter((v) => v !== item.lwpoCd)
|
||||
: [...prev, item.lwpoCd],
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@@ -396,7 +422,7 @@ function CnfmListPage() {
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
type="button"
|
||||
className="rounded bg-blue-500 px-5 py-2 text-[1.3rem] font-semibold text-white hover:bg-blue-600 disabled:cursor-not-allowed disabled:bg-gray-300"
|
||||
onClick={handleConfirmTaskDialog}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { CnfmRegistPage } from "./cnfm-regist-page";
|
||||
|
||||
import { InstlSncnApi } from "@/entities/instl-sncn";
|
||||
import { instlSncnService } from "@/entities/instl-sncn";
|
||||
|
||||
import { CnfmRegistPage } from "./cnfm-regist-page";
|
||||
|
||||
import "./cnfm-page.css";
|
||||
|
||||
@@ -11,7 +12,7 @@ function CnfmModifyPage({ id }: { id: string }) {
|
||||
const query = useQuery({
|
||||
// share cache with view page
|
||||
queryKey: ["instl-sncn-detail", id],
|
||||
queryFn: () => InstlSncnApi.getById(id),
|
||||
queryFn: () => instlSncnService.getById(id),
|
||||
});
|
||||
|
||||
if (query.isLoading) {
|
||||
|
||||
@@ -21,12 +21,12 @@ import { cnstpSeMngService } from "@/entities/cnstp";
|
||||
import { esntlAtchFileMngService, type EsntlAtchFileMngTreeResponseDto } from "@/entities/esntl";
|
||||
import {
|
||||
INSTL_SNCN_REGISTER_SELECTION_KEY,
|
||||
InstlSncnApi,
|
||||
type InstlSncnApplySaveRequestDto,
|
||||
type InstlSncnBizCstSaveRequestDto,
|
||||
type InstlSncnBzScaleSaveDto,
|
||||
type InstlSncnMngResponseDto,
|
||||
InstlSncnMngSaveRequestDto,
|
||||
instlSncnService,
|
||||
} from "@/entities/instl-sncn";
|
||||
import { EtcAttachments, NumCell, PrivacyConsent, ReadCell } from "@/widgets/cnfm";
|
||||
import { Button, Input } from "@common/components";
|
||||
@@ -1170,7 +1170,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
|
||||
};
|
||||
|
||||
try {
|
||||
const issuedDocNo = await InstlSncnApi.saveApply(payload);
|
||||
const issuedDocNo = await instlSncnService.saveApply(payload);
|
||||
alert(`저장되었습니다. 문서번호: ${issuedDocNo}`);
|
||||
} catch (error) {
|
||||
console.error("저장 API 호출 실패", error);
|
||||
@@ -1194,7 +1194,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
|
||||
};
|
||||
|
||||
try {
|
||||
const issuedDocNo = await InstlSncnApi.saveApply(payload);
|
||||
const issuedDocNo = await instlSncnService.saveApply(payload);
|
||||
alert(`제출 되었습니다. 문서번호: ${issuedDocNo}`);
|
||||
} catch (error) {
|
||||
console.error("저장 API 호출 실패", error);
|
||||
|
||||
@@ -14,7 +14,7 @@ import axios from "axios";
|
||||
import { processElaprPrgrs } from "@/entities/elapr/api/elapr-prgrs-service";
|
||||
import { ElaprDmndApi } from "@/entities/elapr/api/elapr-dmnd-service";
|
||||
import { dayjs, useUserStore, useAlertStore } from "@/shared/lib";
|
||||
import { InstlSncnApi, type InstlSncnApplySaveRequestDto } from "@/entities/instl-sncn";
|
||||
import { instlSncnService, type InstlSncnApplySaveRequestDto } from "@/entities/instl-sncn";
|
||||
import { client } from "@/shared/api";
|
||||
|
||||
import "./cnfm-page.css";
|
||||
@@ -99,7 +99,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["instl-sncn-detail", id],
|
||||
queryFn: () => InstlSncnApi.getById(id),
|
||||
queryFn: () => instlSncnService.getById(id),
|
||||
});
|
||||
|
||||
const data = (query.data ?? {}) as any;
|
||||
@@ -382,7 +382,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
} as unknown as InstlSncnApplySaveRequestDto;
|
||||
|
||||
try {
|
||||
const issuedDocNo = await InstlSncnApi.techReview(det.instlSncnDocNo, payload);
|
||||
const issuedDocNo = await instlSncnService.techReview(det.instlSncnDocNo, payload);
|
||||
alert(`기술검토 등록이 완료되었습니다. 문서번호: ${issuedDocNo}`);
|
||||
} catch (error) {
|
||||
console.error("기술검토 등록 API 호출 실패", error);
|
||||
|
||||
@@ -50,7 +50,7 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, InstlSncnViewBut
|
||||
},
|
||||
};
|
||||
|
||||
const ROLE_USER01_ATRZ_STATUS_CODES = ["ER", "EE"] as const;
|
||||
const ROLE_USER01_ATRZ_STATUS_CODES = ["ER", "TE"] as const;
|
||||
|
||||
const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record<
|
||||
string,
|
||||
@@ -59,7 +59,7 @@ const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record<
|
||||
ER: {
|
||||
ATRZ01: [INSTL_SNCN_VIEW_BUTTON_ACTION.dsgntRvwr],
|
||||
},
|
||||
EE: {
|
||||
TE: {
|
||||
ATRZ02: [
|
||||
INSTL_SNCN_VIEW_BUTTON_ACTION.rvwStrg,
|
||||
INSTL_SNCN_VIEW_BUTTON_ACTION.rvwCmptn,
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import type {
|
||||
BizCstTreeDto,
|
||||
BizSclTreeDto,
|
||||
CklstRvwRsltTreeDto,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
InstlSncnApplySaveRequestDto,
|
||||
InstlSncnBizCstSaveRequestDto,
|
||||
InstlSncnBzScaleSaveDto,
|
||||
InstlSncnCklstResultSaveDto,
|
||||
InstlSncnEsntlAtchFileSaveDto,
|
||||
InstlSncnMngDetResponse,
|
||||
InstlSncnMngResponseDto,
|
||||
ReviewStepCode,
|
||||
} from "@/entities/instl-sncn";
|
||||
|
||||
import {
|
||||
type ChecklistFormState,
|
||||
type ChecklistProgress,
|
||||
type ChecklistRowKey,
|
||||
type ChecklistStepMap,
|
||||
INSTL_PROGRESS_ORDER,
|
||||
@@ -16,8 +24,12 @@ import {
|
||||
ROLE_TO_REVIEW_STEP,
|
||||
} from "../model/view-types";
|
||||
|
||||
export function buildChecklistRowKey(row: CklstRvwRsltTreeDto): ChecklistRowKey {
|
||||
return `${row.taskSeComCd ?? ""}_${row.cklstSn}_${row.cklstArtclSn ?? ""}_${row.cklstRvwSn}_${row.rvwStpComCd ?? "LOCAL"}`;
|
||||
export function buildChecklistRowKey(
|
||||
row: CklstRvwRsltTreeDto,
|
||||
reviewStep?: ReviewStepCode,
|
||||
): ChecklistRowKey {
|
||||
const step = reviewStep ?? (row.rvwStpComCd as ReviewStepCode) ?? "LOCAL";
|
||||
return `${row.taskSeComCd}_${row.cklstSn}_${row.cklstArtclSn}_${row.cklstRvwSn}_${step}`;
|
||||
}
|
||||
|
||||
export function groupByReviewStep(rows: CklstRvwRsltTreeDto[]): ChecklistStepMap {
|
||||
@@ -30,11 +42,9 @@ export function groupByReviewStep(rows: CklstRvwRsltTreeDto[]): ChecklistStepMap
|
||||
}
|
||||
|
||||
export function getProgressStepCount(prgrsSttsComCd?: string): number {
|
||||
const idx = INSTL_PROGRESS_ORDER.indexOf(
|
||||
(prgrsSttsComCd ?? "ST") as (typeof INSTL_PROGRESS_ORDER)[number],
|
||||
);
|
||||
if (idx <= INSTL_PROGRESS_ORDER.indexOf("ST")) return 1;
|
||||
if (idx <= INSTL_PROGRESS_ORDER.indexOf("TP")) return 2;
|
||||
const idx = INSTL_PROGRESS_ORDER.indexOf(prgrsSttsComCd as (typeof INSTL_PROGRESS_ORDER)[number]);
|
||||
if (idx <= INSTL_PROGRESS_ORDER.indexOf("SV")) return 1;
|
||||
if (idx <= INSTL_PROGRESS_ORDER.indexOf("LE")) return 2;
|
||||
return 3;
|
||||
}
|
||||
|
||||
@@ -70,6 +80,62 @@ export function filterChecklistRows(
|
||||
);
|
||||
}
|
||||
|
||||
export function getChecklistDisplayRows(
|
||||
rows: CklstRvwRsltTreeDto[],
|
||||
taskCode?: string | null,
|
||||
): CklstRvwRsltTreeDto[] {
|
||||
const filtered = filterChecklistRows(rows, taskCode ?? null);
|
||||
const seen = new Set<string>();
|
||||
const deduped: CklstRvwRsltTreeDto[] = [];
|
||||
|
||||
for (const row of filtered) {
|
||||
const dedupeKey = `${row.taskSeComCd}_${row.cklstRvwSn}`;
|
||||
if (seen.has(dedupeKey)) continue;
|
||||
seen.add(dedupeKey);
|
||||
deduped.push(row);
|
||||
}
|
||||
|
||||
return deduped;
|
||||
}
|
||||
|
||||
export function computeChecklistProgress(
|
||||
rows: CklstRvwRsltTreeDto[],
|
||||
results: ChecklistFormState["results"],
|
||||
): ChecklistProgress {
|
||||
const activeRows = rows.filter((row) => row.bchckUseYn === "Y");
|
||||
const total = activeRows.length;
|
||||
const completed = activeRows.reduce((acc, row) => {
|
||||
const key = buildChecklistRowKey(row);
|
||||
const val = results[key] ?? row.rvwRsltAnmlyYn ?? "";
|
||||
return acc + (val ? 1 : 0);
|
||||
}, 0);
|
||||
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
|
||||
return { total, completed, percent };
|
||||
}
|
||||
|
||||
export function filterChecklistRowsByReviewStep(
|
||||
rows: CklstRvwRsltTreeDto[],
|
||||
reviewStep: ReviewStepCode,
|
||||
): CklstRvwRsltTreeDto[] {
|
||||
return rows.filter((row) => row.rvwStpComCd === reviewStep);
|
||||
}
|
||||
|
||||
export function computeReviewStepChecklistProgress(
|
||||
rows: CklstRvwRsltTreeDto[],
|
||||
results: ChecklistFormState["results"],
|
||||
reviewStep: ReviewStepCode,
|
||||
): ChecklistProgress {
|
||||
const activeRows = getChecklistDisplayRows(rows).filter((row) => row.bchckUseYn === "Y");
|
||||
const total = activeRows.length;
|
||||
const completed = activeRows.reduce((acc, row) => {
|
||||
const key = buildChecklistRowKey(row, reviewStep);
|
||||
const val = results[key];
|
||||
return acc + (val === "Y" || val === "N" ? 1 : 0);
|
||||
}, 0);
|
||||
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
|
||||
return { total, completed, percent };
|
||||
}
|
||||
|
||||
export function buildChecklistFormState(rows: CklstRvwRsltTreeDto[]): ChecklistFormState {
|
||||
const results: ChecklistFormState["results"] = {};
|
||||
const comments: ChecklistFormState["comments"] = {};
|
||||
@@ -80,7 +146,57 @@ export function buildChecklistFormState(rows: CklstRvwRsltTreeDto[]): ChecklistF
|
||||
}
|
||||
if (row.picRvwCn) comments[key] = row.picRvwCn;
|
||||
});
|
||||
return { results, comments };
|
||||
return {
|
||||
results,
|
||||
comments,
|
||||
progress: computeChecklistProgress(rows, results),
|
||||
};
|
||||
}
|
||||
|
||||
export function mergeChecklistFormStateFromServer(
|
||||
prev: ChecklistFormState,
|
||||
allRows: CklstRvwRsltTreeDto[],
|
||||
): ChecklistFormState {
|
||||
const results = { ...prev.results };
|
||||
const comments = { ...prev.comments };
|
||||
|
||||
for (const row of allRows) {
|
||||
const step = row.rvwStpComCd as ReviewStepCode | undefined;
|
||||
if (!step || !REVIEW_STEP_CODES.includes(step)) continue;
|
||||
|
||||
const key = buildChecklistRowKey(row);
|
||||
if ((row.rvwRsltAnmlyYn === "Y" || row.rvwRsltAnmlyYn === "N") && results[key] === undefined) {
|
||||
results[key] = row.rvwRsltAnmlyYn;
|
||||
}
|
||||
if (row.picRvwCn && !comments[key]) {
|
||||
comments[key] = row.picRvwCn;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
results,
|
||||
comments,
|
||||
progress: computeChecklistProgress(getChecklistDisplayRows(allRows), results),
|
||||
};
|
||||
}
|
||||
|
||||
export function patchChecklistFormState(
|
||||
prev: ChecklistFormState,
|
||||
patch: Partial<Pick<ChecklistFormState, "results" | "comments">>,
|
||||
allRows: CklstRvwRsltTreeDto[],
|
||||
reviewStep?: ReviewStepCode,
|
||||
): ChecklistFormState {
|
||||
const results = patch.results ?? prev.results;
|
||||
const comments = patch.comments ?? prev.comments;
|
||||
const progress = reviewStep
|
||||
? computeReviewStepChecklistProgress(allRows, results, reviewStep)
|
||||
: computeChecklistProgress(getChecklistDisplayRows(allRows), results);
|
||||
|
||||
return {
|
||||
results,
|
||||
comments,
|
||||
progress,
|
||||
};
|
||||
}
|
||||
|
||||
export function toCklstSaveDto(
|
||||
@@ -88,24 +204,92 @@ export function toCklstSaveDto(
|
||||
formState: ChecklistFormState,
|
||||
reviewStep: ReviewStepCode,
|
||||
): InstlSncnCklstResultSaveDto[] {
|
||||
return rows
|
||||
return getChecklistDisplayRows(rows)
|
||||
.filter((row) => row.bchckUseYn === "Y")
|
||||
.map((row) => {
|
||||
const key = buildChecklistRowKey(row);
|
||||
const selected =
|
||||
formState.results[key] ??
|
||||
(row.rvwRsltAnmlyYn === "Y" || row.rvwRsltAnmlyYn === "N" ? row.rvwRsltAnmlyYn : null);
|
||||
const key = buildChecklistRowKey(row, reviewStep);
|
||||
const selected = formState.results[key] ?? null;
|
||||
return {
|
||||
cklstSn: row.cklstSn,
|
||||
cklstArtclSn: row.cklstArtclSn,
|
||||
cklstRvwSn: row.cklstRvwSn,
|
||||
rvwStpComCd: reviewStep,
|
||||
selected,
|
||||
picRvwCn: formState.comments[key] ?? row.picRvwCn ?? null,
|
||||
picRvwCn: formState.comments[key] ?? null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function toBzScaleSaveDto(bizSclList: BizSclTreeDto[]): InstlSncnBzScaleSaveDto[] {
|
||||
return bizSclList
|
||||
.filter((node) => node.useYn === "Y")
|
||||
.map((node) => ({
|
||||
cnstpSeCd: node.cnstpSeCd,
|
||||
bscPl: node.bscPlanQty,
|
||||
budgetReflct: node.bgtRfltQty,
|
||||
thtiDsgn: node.tstmDesignQty,
|
||||
diff: node.tstmDesignQty - node.bgtRfltQty,
|
||||
}));
|
||||
}
|
||||
|
||||
export function toBizCstSaveDto(bizCstList: BizCstTreeDto[]): InstlSncnBizCstSaveRequestDto[] {
|
||||
return bizCstList
|
||||
.filter((node) => node.useYn === "Y")
|
||||
.map((node) => ({
|
||||
acntgSeCd: node.acntgSeCd,
|
||||
bscPlanAmt: node.bscPlanAmt,
|
||||
bgtRfltAmt: node.bgtRfltAmt,
|
||||
tstmDesignAmt: node.tstmDesignAmt,
|
||||
}));
|
||||
}
|
||||
|
||||
export function toEsntlAtchFileSaveDto(
|
||||
esntlFileList: EsntlAtchFileMngTreeResponseDto[],
|
||||
): InstlSncnEsntlAtchFileSaveDto[] {
|
||||
const esntlMap = new Map<string, string>();
|
||||
|
||||
for (const item of esntlFileList) {
|
||||
if (!item.esntlAtchFileId || !item.atflCd) continue;
|
||||
esntlMap.set(item.esntlAtchFileId, item.atflCd);
|
||||
}
|
||||
|
||||
return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({
|
||||
esntlAtchFileId,
|
||||
atchFileGroupId,
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildSavePayload(
|
||||
data: InstlSncnMngDetResponse,
|
||||
checklistFormState: ChecklistFormState,
|
||||
reviewStep: ReviewStepCode,
|
||||
prgrsSttsComCd: string,
|
||||
): InstlSncnApplySaveRequestDto {
|
||||
const { detDto, bizSclList, bizCstList, cklstResultList = [], esntlFileList } = data;
|
||||
|
||||
return {
|
||||
instlSncnMng: { ...detDto, prgrsSttsComCd },
|
||||
bzScaleList: toBzScaleSaveDto(bizSclList),
|
||||
bizCstList: toBizCstSaveDto(bizCstList),
|
||||
cklstResultList: toCklstSaveDto(cklstResultList, checklistFormState, reviewStep),
|
||||
esntlAtchFileList: toEsntlAtchFileSaveDto(esntlFileList),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTechReviewReplyPayload(
|
||||
data: InstlSncnMngDetResponse,
|
||||
checklistFormState: ChecklistFormState,
|
||||
reviewStep: ReviewStepCode,
|
||||
prgrsSttsComCd: string,
|
||||
techReViewSaveCd: string,
|
||||
): InstlSncnApplySaveRequestDto {
|
||||
return {
|
||||
...buildSavePayload(data, checklistFormState, reviewStep, prgrsSttsComCd),
|
||||
techReViewSaveCd,
|
||||
rvwStpCd: reviewStep,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTechReviewPayload(
|
||||
det: InstlSncnMngResponseDto,
|
||||
cklstResultList: InstlSncnCklstResultSaveDto[],
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import type { CklstRvwRsltTreeDto, ReviewStepCode } from "@/entities/instl-sncn";
|
||||
|
||||
import { type ChecklistFormState, DEFAULT_CHECKLIST_PROGRESS } from "../model/view-types";
|
||||
|
||||
import { mergeChecklistFormStateFromServer, patchChecklistFormState } from "./checklist-utils";
|
||||
|
||||
const EMPTY_CKLST_RESULT_LIST: CklstRvwRsltTreeDto[] = [];
|
||||
|
||||
function isSameChecklistFormState(a: ChecklistFormState, b: ChecklistFormState): boolean {
|
||||
if (
|
||||
a.progress.total !== b.progress.total ||
|
||||
a.progress.completed !== b.progress.completed ||
|
||||
a.progress.percent !== b.progress.percent
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const aResultKeys = Object.keys(a.results);
|
||||
if (aResultKeys.length !== Object.keys(b.results).length) return false;
|
||||
if (!aResultKeys.every((key) => a.results[key] === b.results[key])) return false;
|
||||
|
||||
const aCommentKeys = Object.keys(a.comments);
|
||||
if (aCommentKeys.length !== Object.keys(b.comments).length) return false;
|
||||
|
||||
return aCommentKeys.every((key) => a.comments[key] === b.comments[key]);
|
||||
}
|
||||
|
||||
function useChecklistFormState(cklstResultList: CklstRvwRsltTreeDto[] = EMPTY_CKLST_RESULT_LIST) {
|
||||
const [formState, setFormState] = useState<ChecklistFormState>({
|
||||
results: {},
|
||||
comments: {},
|
||||
progress: DEFAULT_CHECKLIST_PROGRESS,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setFormState((prev) => {
|
||||
const next = mergeChecklistFormStateFromServer(prev, cklstResultList);
|
||||
return isSameChecklistFormState(prev, next) ? prev : next;
|
||||
});
|
||||
}, [cklstResultList]);
|
||||
|
||||
const patchFormState = useCallback(
|
||||
(
|
||||
patch: Partial<Pick<ChecklistFormState, "results" | "comments">>,
|
||||
reviewStep?: ReviewStepCode,
|
||||
) => {
|
||||
setFormState((prev) => patchChecklistFormState(prev, patch, cklstResultList, reviewStep));
|
||||
},
|
||||
[cklstResultList],
|
||||
);
|
||||
|
||||
return { formState, setFormState, patchFormState };
|
||||
}
|
||||
|
||||
export { EMPTY_CKLST_RESULT_LIST, useChecklistFormState };
|
||||
@@ -26,9 +26,22 @@ export type {
|
||||
|
||||
export type ChecklistRowKey = string;
|
||||
|
||||
export interface ChecklistProgress {
|
||||
total: number;
|
||||
completed: number;
|
||||
percent: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_CHECKLIST_PROGRESS: ChecklistProgress = {
|
||||
total: 0,
|
||||
completed: 0,
|
||||
percent: 0,
|
||||
};
|
||||
|
||||
export interface ChecklistFormState {
|
||||
results: Record<ChecklistRowKey, "Y" | "N">;
|
||||
comments: Record<ChecklistRowKey, string>;
|
||||
progress: ChecklistProgress;
|
||||
}
|
||||
|
||||
export type ChecklistStepMap = Record<ReviewStepCode, CklstRvwRsltTreeDto[]>;
|
||||
@@ -48,17 +61,23 @@ export const TASK_CODE_LABELS: Record<string, string> = {
|
||||
};
|
||||
|
||||
export const INSTL_PROGRESS_ORDER = [
|
||||
// ROLE_USER05
|
||||
"SV",
|
||||
"ST",
|
||||
"LE",
|
||||
"ER",
|
||||
"EE",
|
||||
"TE",
|
||||
"TS",
|
||||
"TR",
|
||||
"TP",
|
||||
"EP",
|
||||
"SL",
|
||||
"TA",
|
||||
|
||||
// ROLE_USER04
|
||||
"ST",
|
||||
"EP",
|
||||
|
||||
// ROLE_USER03
|
||||
"LE",
|
||||
"TP",
|
||||
|
||||
// ROLE_USER01
|
||||
"ER",
|
||||
"TE",
|
||||
|
||||
"TB",
|
||||
] as const;
|
||||
|
||||
|
||||
@@ -12,12 +12,13 @@ import {
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
import { buildChecklistRowKey } from "../../lib/checklist-utils";
|
||||
import { buildChecklistRowKey, patchChecklistFormState } from "../../lib/checklist-utils";
|
||||
import { type ChecklistFormState, ROLE_TO_REVIEW_STEP } from "../../model/view-types";
|
||||
|
||||
interface ChecklistTaskPanelProps {
|
||||
selectedReviewStep: ReviewStepCode;
|
||||
selectedRows: CklstRvwRsltTreeDto[];
|
||||
allRows: CklstRvwRsltTreeDto[];
|
||||
formState: ChecklistFormState;
|
||||
setFormState: Dispatch<SetStateAction<ChecklistFormState>>;
|
||||
userRoleCode?: string;
|
||||
@@ -26,6 +27,7 @@ interface ChecklistTaskPanelProps {
|
||||
function ChecklistTaskPanel({
|
||||
selectedReviewStep,
|
||||
selectedRows,
|
||||
allRows,
|
||||
formState,
|
||||
setFormState,
|
||||
userRoleCode,
|
||||
@@ -64,7 +66,7 @@ function ChecklistTaskPanel({
|
||||
</TableRow>
|
||||
) : (
|
||||
selectedRows.map((row) => {
|
||||
const key = buildChecklistRowKey(row);
|
||||
const key = buildChecklistRowKey(row, selectedReviewStep);
|
||||
const isFieldVisible = row.bchckUseYn === "Y";
|
||||
const isLocalReviewStep = selectedReviewStep === "LOCAL";
|
||||
|
||||
@@ -74,16 +76,11 @@ function ChecklistTaskPanel({
|
||||
<TableCell
|
||||
className="pl-2 !text-left text-[1.2rem] text-gray-600"
|
||||
style={{ paddingLeft: `${Math.max(0, row.treeLevel - 1) * 16 + 8}px` }}
|
||||
colSpan={isFieldVisible ? 0 : isLocalReviewStep ? 3 : 4}
|
||||
>
|
||||
{row.treeLevel > 1 ? "└ " : ""}
|
||||
{row.rvwCn}
|
||||
</TableCell>
|
||||
{!isFieldVisible ? (
|
||||
<>
|
||||
<TableCell className="bg-muted" />
|
||||
<TableCell className="bg-muted" />
|
||||
</>
|
||||
) : (
|
||||
{isFieldVisible && (
|
||||
<>
|
||||
<TableCell className="text-center">
|
||||
<input
|
||||
@@ -94,10 +91,14 @@ function ChecklistTaskPanel({
|
||||
readOnly={!isStepEditable}
|
||||
onChange={() => {
|
||||
if (!isStepEditable) return;
|
||||
setFormState((prev) => ({
|
||||
...prev,
|
||||
results: { ...prev.results, [key]: "Y" },
|
||||
}));
|
||||
setFormState((prev) =>
|
||||
patchChecklistFormState(
|
||||
prev,
|
||||
{ results: { ...prev.results, [key]: "Y" } },
|
||||
allRows,
|
||||
selectedReviewStep,
|
||||
),
|
||||
);
|
||||
}}
|
||||
className={cn(isStepEditable ? "cursor-pointer" : "cursor-not-allowed")}
|
||||
/>
|
||||
@@ -111,37 +112,43 @@ function ChecklistTaskPanel({
|
||||
readOnly={!isStepEditable}
|
||||
onChange={() => {
|
||||
if (!isStepEditable) return;
|
||||
setFormState((prev) => ({
|
||||
...prev,
|
||||
results: { ...prev.results, [key]: "N" },
|
||||
}));
|
||||
setFormState((prev) =>
|
||||
patchChecklistFormState(
|
||||
prev,
|
||||
{ results: { ...prev.results, [key]: "N" } },
|
||||
allRows,
|
||||
selectedReviewStep,
|
||||
),
|
||||
);
|
||||
}}
|
||||
className={cn(isStepEditable ? "cursor-pointer" : "cursor-not-allowed")}
|
||||
/>
|
||||
</TableCell>
|
||||
</>
|
||||
)}
|
||||
{!isLocalReviewStep ? (
|
||||
<TableCell className={`text-left ${isFieldVisible ? "" : "bg-muted"}`}>
|
||||
{isFieldVisible && (
|
||||
<Textarea
|
||||
className={cn(
|
||||
"min-h-[6rem] !text-xl",
|
||||
isStepEditable ? "cursor-pointer" : "cursor-not-allowed",
|
||||
)}
|
||||
value={formState.comments[key] ?? ""}
|
||||
readOnly={!isStepEditable}
|
||||
onChange={(event) => {
|
||||
if (!isStepEditable) return;
|
||||
setFormState((prev) => ({
|
||||
...prev,
|
||||
comments: { ...prev.comments, [key]: event.target.value },
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!isLocalReviewStep && isFieldVisible && (
|
||||
<TableCell className="text-left">
|
||||
<Textarea
|
||||
className={cn(
|
||||
"min-h-[6rem] !text-xl",
|
||||
isStepEditable ? "cursor-pointer" : "cursor-not-allowed",
|
||||
)}
|
||||
value={formState.comments[key] ?? ""}
|
||||
readOnly={!isStepEditable}
|
||||
onChange={(event) => {
|
||||
if (!isStepEditable) return;
|
||||
setFormState((prev) =>
|
||||
patchChecklistFormState(
|
||||
prev,
|
||||
{ comments: { ...prev.comments, [key]: event.target.value } },
|
||||
allRows,
|
||||
selectedReviewStep,
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
) : null}
|
||||
)}
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -8,7 +8,13 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import { ElaprDmndApi, ElaprDmndSaveRequest, type ElaprTmplt } from "@/entities/elapr";
|
||||
import { instlSncnKeys, type InstlSncnMngDetResponse } from "@/entities/instl-sncn";
|
||||
import {
|
||||
type DocPrgrsUpdateRequestDto,
|
||||
type InstlSncnApplySaveRequestDto,
|
||||
instlSncnKeys,
|
||||
type InstlSncnMngDetResponse,
|
||||
instlSncnService,
|
||||
} from "@/entities/instl-sncn";
|
||||
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
@@ -18,24 +24,34 @@ import {
|
||||
type InstlSncnViewButtonAction,
|
||||
needsRoleUser01AtrzResolve,
|
||||
} from "../../lib/button-config";
|
||||
import {
|
||||
buildSavePayload,
|
||||
buildTechReviewReplyPayload,
|
||||
computeReviewStepChecklistProgress,
|
||||
} from "../../lib/checklist-utils";
|
||||
import { type ChecklistFormState, ROLE_TO_REVIEW_STEP } from "../../model/view-types";
|
||||
|
||||
import { ElaprTmpltPickerDialog } from "./elapr-tmplt-picker-dialog";
|
||||
import { TextInputDialog } from "./text-input-dialog";
|
||||
|
||||
interface FloatingActionButtonsProps {
|
||||
data: InstlSncnMngDetResponse;
|
||||
docSeComCd: string;
|
||||
instlSncnDocNo: string;
|
||||
prgrsSttsComCd: string;
|
||||
dtlInstCd: string | undefined;
|
||||
checklistFormState: ChecklistFormState;
|
||||
}
|
||||
|
||||
type ActiveDialog = "elaprTmplt" | "rjct" | "dmndSplmnt" | null;
|
||||
|
||||
function FloatingActionButtons({
|
||||
data,
|
||||
docSeComCd,
|
||||
instlSncnDocNo,
|
||||
prgrsSttsComCd,
|
||||
dtlInstCd,
|
||||
checklistFormState,
|
||||
}: FloatingActionButtonsProps) {
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
@@ -45,6 +61,7 @@ function FloatingActionButtons({
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [activeDialog, setActiveDialog] = useState<ActiveDialog>(null);
|
||||
|
||||
const docNo = data.detDto.instlSncnDocNo;
|
||||
const isRegister = data.detDto.rgtr === userInfo?.userUnqId;
|
||||
|
||||
const needsAtrzResolve = needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd);
|
||||
@@ -110,8 +127,51 @@ function FloatingActionButtons({
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStrgClick() {
|
||||
const payload: InstlSncnApplySaveRequestDto = buildSavePayload(
|
||||
data,
|
||||
checklistFormState,
|
||||
"LOCAL",
|
||||
"SV",
|
||||
);
|
||||
|
||||
await executeAction({
|
||||
action: INSTL_SNCN_VIEW_BUTTON_ACTION.strg,
|
||||
request: payload,
|
||||
successMessage: "저장되었습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
async function handleAplyClick() {
|
||||
const { percent } = computeReviewStepChecklistProgress(
|
||||
data.cklstResultList ?? [],
|
||||
checklistFormState.results,
|
||||
"LOCAL",
|
||||
);
|
||||
const payload: InstlSncnApplySaveRequestDto = buildSavePayload(
|
||||
data,
|
||||
checklistFormState,
|
||||
"LOCAL",
|
||||
"ST",
|
||||
);
|
||||
|
||||
if (percent !== 100) {
|
||||
alert("모든 체크리스트 항목을 체크해야 신청이 가능합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
await executeAction({
|
||||
action: INSTL_SNCN_VIEW_BUTTON_ACTION.aply,
|
||||
request: payload,
|
||||
successMessage: "신청 완료되었습니다.",
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: instlSncnKeys.list(),
|
||||
});
|
||||
}
|
||||
|
||||
async function handleDelClick() {
|
||||
if (!(await confirm("정말 삭제하시겠습니까?"))) return;
|
||||
if (!(await confirm("삭제하시겠습니까?"))) return;
|
||||
await executeAction({
|
||||
action: INSTL_SNCN_VIEW_BUTTON_ACTION.del,
|
||||
successMessage: "삭제되었습니다.",
|
||||
@@ -119,16 +179,108 @@ function FloatingActionButtons({
|
||||
router.push("/instl-sncn");
|
||||
}
|
||||
|
||||
function handleRjctClick() {
|
||||
setActiveDialog("rjct");
|
||||
async function handleInstlSncnAncmntClick() {
|
||||
const payload: DocPrgrsUpdateRequestDto = {
|
||||
docNo,
|
||||
docSeComCd,
|
||||
prgrsSttsComCd: "TB",
|
||||
};
|
||||
await executeAction({
|
||||
action: INSTL_SNCN_VIEW_BUTTON_ACTION.instlSncnAncmnt,
|
||||
request: payload,
|
||||
successMessage: "고시되었습니다.",
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: instlSncnKeys.list(),
|
||||
});
|
||||
}
|
||||
|
||||
async function handleRjctConfirm(rjctRsn: string) {
|
||||
setActiveDialog(null);
|
||||
async function handleEnvoSbmsnClick() {
|
||||
const payload: DocPrgrsUpdateRequestDto = {
|
||||
docNo,
|
||||
docSeComCd,
|
||||
prgrsSttsComCd: "LE",
|
||||
};
|
||||
await executeAction({
|
||||
action: INSTL_SNCN_VIEW_BUTTON_ACTION.envoSbmsn,
|
||||
request: payload,
|
||||
successMessage: "제출되었습니다.",
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: instlSncnKeys.list(),
|
||||
});
|
||||
}
|
||||
|
||||
async function handleRjctClick() {
|
||||
if (!(await confirm("반려하시겠습니까?"))) return;
|
||||
|
||||
const payload: DocPrgrsUpdateRequestDto = {
|
||||
docNo,
|
||||
docSeComCd,
|
||||
prgrsSttsComCd: "SV",
|
||||
};
|
||||
await executeAction({
|
||||
action: INSTL_SNCN_VIEW_BUTTON_ACTION.rjct,
|
||||
request: rjctRsn,
|
||||
successMessage: "반려 처리되었습니다.",
|
||||
request: payload,
|
||||
successMessage: "반려되었습니다.",
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: instlSncnKeys.list(),
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSncnAprvClick() {
|
||||
const payload: DocPrgrsUpdateRequestDto = {
|
||||
docNo,
|
||||
docSeComCd,
|
||||
prgrsSttsComCd: "TA",
|
||||
};
|
||||
await executeAction({
|
||||
action: INSTL_SNCN_VIEW_BUTTON_ACTION.sncnAprv,
|
||||
request: payload,
|
||||
successMessage: "인가승인되었습니다.",
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: instlSncnKeys.list(),
|
||||
});
|
||||
}
|
||||
|
||||
async function handleRvwRplyClick() {
|
||||
const { percent } = computeReviewStepChecklistProgress(
|
||||
data.cklstResultList ?? [],
|
||||
checklistFormState.results,
|
||||
"ENV",
|
||||
);
|
||||
|
||||
if (percent !== 100) {
|
||||
alert("모든 체크리스트 항목을 체크해야 검토결과 회신이 가능합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await confirm("검토결과를 회신하시겠습니까?"))) return;
|
||||
|
||||
const payload = buildTechReviewReplyPayload(data, checklistFormState, "ENV", "EP", "CP");
|
||||
|
||||
await executeAction({
|
||||
action: INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply,
|
||||
request: payload,
|
||||
successMessage: "검토결과 회신이 완료되었습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
async function handleRqstTechRvwClick() {
|
||||
const payload: DocPrgrsUpdateRequestDto = {
|
||||
docNo,
|
||||
docSeComCd,
|
||||
prgrsSttsComCd: "ER",
|
||||
};
|
||||
await executeAction({
|
||||
action: INSTL_SNCN_VIEW_BUTTON_ACTION.rqstTechRvw,
|
||||
request: payload,
|
||||
successMessage: "기술검토 의뢰가 완료되었습니다.",
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: instlSncnKeys.list(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -146,6 +298,16 @@ function FloatingActionButtons({
|
||||
});
|
||||
}
|
||||
|
||||
async function handleRvwStrgClick() {
|
||||
const payload = buildTechReviewReplyPayload(data, checklistFormState, "KECO", "TE", "SV");
|
||||
|
||||
await executeAction({
|
||||
action: INSTL_SNCN_VIEW_BUTTON_ACTION.rvwStrg,
|
||||
request: payload,
|
||||
successMessage: "저장되었습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
function handleDmndSplmntClick() {
|
||||
setActiveDialog("dmndSplmnt");
|
||||
}
|
||||
@@ -172,19 +334,12 @@ function FloatingActionButtons({
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.strg) && (
|
||||
<Button
|
||||
onClick={() => void executeAction({ action: INSTL_SNCN_VIEW_BUTTON_ACTION.strg })}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
<Button onClick={handleStrgClick} disabled={isProcessing}>
|
||||
저장
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.aply) && (
|
||||
<Button
|
||||
color="green"
|
||||
onClick={() => void executeAction({ action: INSTL_SNCN_VIEW_BUTTON_ACTION.aply })}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
<Button color="green" onClick={handleAplyClick} disabled={isProcessing}>
|
||||
신청완료
|
||||
</Button>
|
||||
)}
|
||||
@@ -204,22 +359,12 @@ function FloatingActionButtons({
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.instlSncnAncmnt) && (
|
||||
<Button
|
||||
onClick={() =>
|
||||
void executeAction({ action: INSTL_SNCN_VIEW_BUTTON_ACTION.instlSncnAncmnt })
|
||||
}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
<Button onClick={handleInstlSncnAncmntClick} disabled={isProcessing}>
|
||||
설치인가 고시
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.envoSbmsn) && (
|
||||
<Button
|
||||
onClick={() =>
|
||||
void executeAction({ action: INSTL_SNCN_VIEW_BUTTON_ACTION.envoSbmsn })
|
||||
}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
<Button onClick={handleEnvoSbmsnClick} disabled={isProcessing}>
|
||||
환경청제출
|
||||
</Button>
|
||||
)}
|
||||
@@ -229,29 +374,17 @@ function FloatingActionButtons({
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.sncnAprv) && (
|
||||
<Button
|
||||
onClick={() => void executeAction({ action: INSTL_SNCN_VIEW_BUTTON_ACTION.sncnAprv })}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
<Button onClick={handleSncnAprvClick} disabled={isProcessing}>
|
||||
인가승인
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply) && (
|
||||
<Button
|
||||
onClick={() => void executeAction({ action: INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply })}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
<Button color="green" onClick={handleRvwRplyClick} disabled={isProcessing}>
|
||||
검토결과 회신
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.rqstTechRvw) && (
|
||||
<Button
|
||||
color="white"
|
||||
onClick={() =>
|
||||
void executeAction({ action: INSTL_SNCN_VIEW_BUTTON_ACTION.rqstTechRvw })
|
||||
}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
<Button onClick={handleRqstTechRvwClick} disabled={isProcessing}>
|
||||
기술검토 의뢰
|
||||
</Button>
|
||||
)}
|
||||
@@ -261,10 +394,7 @@ function FloatingActionButtons({
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.rvwStrg) && (
|
||||
<Button
|
||||
onClick={() => void executeAction({ action: INSTL_SNCN_VIEW_BUTTON_ACTION.rvwStrg })}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
<Button onClick={handleRvwStrgClick} disabled={isProcessing}>
|
||||
저장
|
||||
</Button>
|
||||
)}
|
||||
@@ -315,14 +445,6 @@ function FloatingActionButtons({
|
||||
onClose={() => setActiveDialog(null)}
|
||||
onSelect={handleElaprTmpltSelect}
|
||||
/>
|
||||
<TextInputDialog
|
||||
open={activeDialog === "rjct"}
|
||||
onOpenChange={() => setActiveDialog(null)}
|
||||
title="반려 사유 입력"
|
||||
placeholder="반려 사유를 입력하세요."
|
||||
isSubmitting={isProcessing}
|
||||
onConfirm={(value) => void handleRjctConfirm(value)}
|
||||
/>
|
||||
<TextInputDialog
|
||||
open={activeDialog === "dmndSplmnt"}
|
||||
onOpenChange={() => setActiveDialog(null)}
|
||||
@@ -337,14 +459,26 @@ function FloatingActionButtons({
|
||||
|
||||
export { FloatingActionButtons };
|
||||
|
||||
async function handleStrg(context: ActionHandlerContext): Promise<void> {
|
||||
// TODO: 저장 API 호출
|
||||
context.onSuccess?.();
|
||||
async function handleStrg(
|
||||
context: ActionHandlerContext<InstlSncnApplySaveRequestDto>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await instlSncnService.saveApply(context.request);
|
||||
context.onSuccess?.();
|
||||
} catch (error) {
|
||||
context.onError?.(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAply(context: ActionHandlerContext): Promise<void> {
|
||||
// TODO: 신청완료 API 호출
|
||||
context.onSuccess?.();
|
||||
async function handleAply(
|
||||
context: ActionHandlerContext<InstlSncnApplySaveRequestDto>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await instlSncnService.saveApply(context.request);
|
||||
context.onSuccess?.();
|
||||
} catch (error) {
|
||||
context.onError?.(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDel(context: ActionHandlerContext): Promise<void> {
|
||||
@@ -358,33 +492,61 @@ async function handleSplmntCmptn(context: ActionHandlerContext): Promise<void> {
|
||||
}
|
||||
|
||||
async function handleInstlSncnAncmnt(context: ActionHandlerContext): Promise<void> {
|
||||
// TODO: 고시 처리 API 호출
|
||||
context.onSuccess?.();
|
||||
try {
|
||||
await instlSncnService.changePrgrsSttsComCd(context.request);
|
||||
context.onSuccess?.();
|
||||
} catch (error) {
|
||||
context.onError?.(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEnvoSbmsn(context: ActionHandlerContext): Promise<void> {
|
||||
// TODO: 환경청제출 API 호출
|
||||
context.onSuccess?.();
|
||||
async function handleEnvoSbmsn(
|
||||
context: ActionHandlerContext<DocPrgrsUpdateRequestDto>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await instlSncnService.changePrgrsSttsComCd(context.request);
|
||||
context.onSuccess?.();
|
||||
} catch (error) {
|
||||
context.onError?.(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRjct(context: ActionHandlerContext): Promise<void> {
|
||||
// TODO: 반려 API 호출
|
||||
context.onSuccess?.();
|
||||
async function handleRjct(context: ActionHandlerContext<DocPrgrsUpdateRequestDto>): Promise<void> {
|
||||
try {
|
||||
await instlSncnService.changePrgrsSttsComCd(context.request);
|
||||
context.onSuccess?.();
|
||||
} catch (error) {
|
||||
context.onError?.(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSncnAprv(context: ActionHandlerContext): Promise<void> {
|
||||
// TODO: 인가승인 API 호출
|
||||
context.onSuccess?.();
|
||||
try {
|
||||
await instlSncnService.changePrgrsSttsComCd(context.request);
|
||||
context.onSuccess?.();
|
||||
} catch (error) {
|
||||
context.onError?.(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRvwRply(context: ActionHandlerContext): Promise<void> {
|
||||
// TODO: 검토결과 회신 API 호출
|
||||
context.onSuccess?.();
|
||||
async function handleRvwRply(
|
||||
context: ActionHandlerContext<InstlSncnApplySaveRequestDto>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await instlSncnService.techReview(context.data.detDto.instlSncnDocNo, context.request);
|
||||
context.onSuccess?.();
|
||||
} catch (error) {
|
||||
context.onError?.(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRqstTechRvw(context: ActionHandlerContext): Promise<void> {
|
||||
// TODO: 기술검토 의뢰 API 호출
|
||||
context.onSuccess?.();
|
||||
try {
|
||||
await instlSncnService.changePrgrsSttsComCd(context.request);
|
||||
context.onSuccess?.();
|
||||
} catch (error) {
|
||||
context.onError?.(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDsgntRvwr(context: ActionHandlerContext<ElaprDmndSaveRequest>): Promise<void> {
|
||||
@@ -396,9 +558,15 @@ async function handleDsgntRvwr(context: ActionHandlerContext<ElaprDmndSaveReques
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRvwStrg(context: ActionHandlerContext): Promise<void> {
|
||||
// TODO: 검토저장 API 호출
|
||||
context.onSuccess?.();
|
||||
async function handleRvwStrg(
|
||||
context: ActionHandlerContext<InstlSncnApplySaveRequestDto>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await instlSncnService.techReview(context.data.detDto.instlSncnDocNo, context.request);
|
||||
context.onSuccess?.();
|
||||
} catch (error) {
|
||||
context.onError?.(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRvwCmptn(context: ActionHandlerContext): Promise<void> {
|
||||
@@ -446,6 +614,7 @@ const buttonActionHandlers: Record<InstlSncnViewButtonAction, ActionHandler> = {
|
||||
grndsChck: handleGrndsChck,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
interface ActionHandlerContext<T = any> {
|
||||
request: T;
|
||||
data: InstlSncnMngDetResponse;
|
||||
|
||||
@@ -9,14 +9,10 @@ interface InstlEditPageProps {
|
||||
}
|
||||
|
||||
function InstlEditPage({ id }: InstlEditPageProps) {
|
||||
const { data, isLoading, isError } = useInstlSncnDetail(id);
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="mt-6 text-[1.4rem]">수정 정보를 불러오는 중...</div>;
|
||||
}
|
||||
const { data, isError } = useInstlSncnDetail(id);
|
||||
|
||||
if (isError || !data) {
|
||||
return <div className="mt-6 text-[1.4rem]">수정 대상을 찾을 수 없습니다.</div>;
|
||||
return <div className="mt-6 text-xl">수정 대상을 찾을 수 없습니다.</div>;
|
||||
}
|
||||
|
||||
return <InstlFormPage mode="edit" initialData={data} />;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useRouter } from "next/navigation";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import { InstlSncnApi, type InstlSncnMngDetResponse } from "@/entities/instl-sncn";
|
||||
import { type InstlSncnMngDetResponse, instlSncnService } from "@/entities/instl-sncn";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import {
|
||||
@@ -67,7 +67,7 @@ function InstlFormPage({ mode, initialData }: InstlFormPageProps) {
|
||||
esntlAtchFileList: data.esntlAtchFileList,
|
||||
};
|
||||
|
||||
const instlSncnDocNo = await InstlSncnApi.saveApply(payload);
|
||||
const instlSncnDocNo = await instlSncnService.saveApply(payload);
|
||||
|
||||
alert(isEditMode ? "수정되었습니다." : "저장되었습니다.");
|
||||
router.push(`/instl-sncn/${instlSncnDocNo}`);
|
||||
|
||||
@@ -4,6 +4,11 @@ import { useUserStore } from "@/shared/lib";
|
||||
import { useInstlSncnDetail } from "@/entities/instl-sncn";
|
||||
import { Skeleton } from "@common/components";
|
||||
|
||||
import {
|
||||
EMPTY_CKLST_RESULT_LIST,
|
||||
useChecklistFormState,
|
||||
} from "../lib/use-checklist-form-state";
|
||||
|
||||
import { FloatingActionButtons } from "./components/floating-action-buttons";
|
||||
import { AttachmentsSection } from "./sections/attachments-section";
|
||||
import { BasicInfoSection } from "./sections/basic-info-section";
|
||||
@@ -20,6 +25,8 @@ interface InstlViewPageProps {
|
||||
function InstlViewPage({ id }: InstlViewPageProps) {
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const { data, isLoading, isError } = useInstlSncnDetail(id);
|
||||
const { formState: checklistFormState, setFormState: setChecklistFormState } =
|
||||
useChecklistFormState(data?.cklstResultList ?? EMPTY_CKLST_RESULT_LIST);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -64,14 +71,18 @@ function InstlViewPage({ id }: InstlViewPageProps) {
|
||||
cklstResultList={cklstResultList}
|
||||
prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
|
||||
userRoleCode={userInfo?.dtlInstCd}
|
||||
formState={checklistFormState}
|
||||
setFormState={setChecklistFormState}
|
||||
/>
|
||||
<AttachmentsSection esntlFileList={esntlFileList} etcFileList={etcFileList} />
|
||||
<TechReviewOpinionSection rvwList={rvwList} />
|
||||
<FloatingActionButtons
|
||||
data={data}
|
||||
docSeComCd="INSTL"
|
||||
instlSncnDocNo={detDto.instlSncnDocNo}
|
||||
prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
|
||||
dtlInstCd={userInfo?.dtlInstCd}
|
||||
checklistFormState={checklistFormState}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
|
||||
import { useAcntgSeMngTree } from "@/entities/acntg";
|
||||
import { Field } from "@/shared/ui/field";
|
||||
import { useAcntgSeMngTree } from "@/entities/acntg";
|
||||
import {
|
||||
Input,
|
||||
Skeleton,
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
|
||||
import { useCnstpSeMngPage } from "@/entities/cnstp";
|
||||
import { Field } from "@/shared/ui/field";
|
||||
import { useCnstpSeMngPage } from "@/entities/cnstp";
|
||||
import {
|
||||
Input,
|
||||
Table,
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { type Dispatch, type SetStateAction, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import type { CklstRvwRsltTreeDto, ReviewStepCode } from "@/entities/instl-sncn";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
|
||||
|
||||
import {
|
||||
buildChecklistFormState,
|
||||
buildChecklistRowKey,
|
||||
computeReviewStepChecklistProgress,
|
||||
extractTaskCodes,
|
||||
filterChecklistRows,
|
||||
getChecklistDisplayRows,
|
||||
getVisibleReviewSteps,
|
||||
} from "../../lib/checklist-utils";
|
||||
import {
|
||||
@@ -24,23 +23,25 @@ interface ChecklistSectionProps {
|
||||
cklstResultList: CklstRvwRsltTreeDto[];
|
||||
prgrsSttsComCd: string;
|
||||
userRoleCode?: string;
|
||||
formState: ChecklistFormState;
|
||||
setFormState: Dispatch<SetStateAction<ChecklistFormState>>;
|
||||
}
|
||||
|
||||
function ChecklistSection({
|
||||
cklstResultList,
|
||||
prgrsSttsComCd,
|
||||
userRoleCode,
|
||||
formState,
|
||||
setFormState,
|
||||
}: ChecklistSectionProps) {
|
||||
const visibleReviewSteps = useMemo(
|
||||
() => getVisibleReviewSteps(prgrsSttsComCd, userRoleCode),
|
||||
[prgrsSttsComCd, userRoleCode],
|
||||
);
|
||||
|
||||
const taskCodes = useMemo(() => extractTaskCodes(cklstResultList), [cklstResultList]);
|
||||
|
||||
const [selectedReviewStep, setSelectedReviewStep] = useState<ReviewStepCode>("LOCAL");
|
||||
const [selectedTaskCode, setSelectedTaskCode] = useState<string | null>(null);
|
||||
const [formState, setFormState] = useState<ChecklistFormState>({ results: {}, comments: {} });
|
||||
|
||||
useEffect(() => {
|
||||
if (visibleReviewSteps.length === 0) return;
|
||||
@@ -59,26 +60,13 @@ function ChecklistSection({
|
||||
}
|
||||
}, [taskCodes, selectedTaskCode]);
|
||||
|
||||
const selectedRows = useMemo(
|
||||
() => filterChecklistRows(cklstResultList, selectedTaskCode),
|
||||
[cklstResultList, selectedTaskCode],
|
||||
const progress = useMemo(
|
||||
() =>
|
||||
computeReviewStepChecklistProgress(cklstResultList, formState.results, selectedReviewStep),
|
||||
[cklstResultList, formState.results, selectedReviewStep],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setFormState(buildChecklistFormState(selectedRows));
|
||||
}, [selectedRows]);
|
||||
|
||||
const cklstStats = useMemo(() => {
|
||||
const rows = selectedRows.filter((row) => row.bchckUseYn === "Y");
|
||||
const total = rows.length;
|
||||
const completed = rows.reduce((acc, row) => {
|
||||
const key = buildChecklistRowKey(row);
|
||||
const val = formState.results[key] ?? row.rvwRsltAnmlyYn ?? "";
|
||||
return acc + (val ? 1 : 0);
|
||||
}, 0);
|
||||
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
|
||||
return { total, completed, percent };
|
||||
}, [selectedRows, formState.results]);
|
||||
const { total, completed, percent } = progress;
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="체크리스트">
|
||||
@@ -119,9 +107,8 @@ function ChecklistSection({
|
||||
))}
|
||||
</TabsList>
|
||||
<div className="shrink-0 pb-1 text-lg text-muted-foreground">
|
||||
총합: <strong>{cklstStats.total}</strong> · 완료:{" "}
|
||||
<strong>{cklstStats.completed}</strong> · 진행률:{" "}
|
||||
<strong>{cklstStats.percent}%</strong>
|
||||
총합: <strong>{total}</strong> · 완료: <strong>{completed}</strong> · 진행률:{" "}
|
||||
<strong>{percent}%</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -129,7 +116,8 @@ function ChecklistSection({
|
||||
<TabsContent key={code} value={code} className="mt-0 border-0 p-0 shadow-none ring-0">
|
||||
<ChecklistTaskPanel
|
||||
selectedReviewStep={selectedReviewStep}
|
||||
selectedRows={selectedRows}
|
||||
selectedRows={getChecklistDisplayRows(cklstResultList, code)}
|
||||
allRows={cklstResultList}
|
||||
formState={formState}
|
||||
setFormState={setFormState}
|
||||
userRoleCode={userRoleCode}
|
||||
|
||||
Reference in New Issue
Block a user