Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m7s

This commit is contained in:
2026-08-10 18:06:03 +09:00
104 changed files with 1529 additions and 817 deletions
@@ -6,11 +6,26 @@ export interface ApproverStatusConfig {
}
export const APPROVER_STATUS_MAP: Record<string, ApproverStatusConfig> = {
ATRZ_PRGRS_001: { label: "대기", badgeClass: "bg-gray-100 text-gray-700" },
ATRZ_PRGRS_002: { label: "요청", badgeClass: "bg-yellow-100 text-yellow-700" },
ATRZ_PRGRS_003: { label: "승인", badgeClass: "bg-green-100 text-green-700" },
ATRZ_PRGRS_004: { label: "반려", badgeClass: "bg-red-100 text-red-700" },
ATRZ_PRGRS_999: { label: "결재안함", badgeClass: "bg-blue-100 text-blue-700" },
ATRZ_PRGRS_001: {
label: "대기",
badgeClass: "bg-gray-100 text-gray-700",
},
ATRZ_PRGRS_002: {
label: "결재요청",
badgeClass: "bg-yellow-100 text-yellow-700",
},
ATRZ_PRGRS_003: {
label: "결재승인",
badgeClass: "bg-green-100 text-green-700",
},
ATRZ_PRGRS_004: {
label: "반려",
badgeClass: "bg-red-100 text-red-700",
},
ATRZ_PRGRS_999: {
label: "결재안함",
badgeClass: "bg-blue-100 text-blue-700",
},
};
export function sortByAtrzSeqNo(
@@ -19,10 +34,19 @@ export function sortByAtrzSeqNo(
return list.slice().sort((a, b) => Number(a.atrzSeqNo ?? 0) - Number(b.atrzSeqNo ?? 0));
}
export function getApproverStatus(status: string | undefined): ApproverStatusConfig {
export function getApproverStatus(
atrzPrgrsSttsComCd?: string,
atrzSeComCd?: string,
): ApproverStatusConfig {
if (atrzPrgrsSttsComCd === "ATRZ_PRGRS_003" && atrzSeComCd === "ATRZ01")
return {
label: "기술검토 접수",
badgeClass: "bg-green-100 text-green-700",
};
return (
APPROVER_STATUS_MAP[status ?? ""] ?? {
label: status || "-",
APPROVER_STATUS_MAP[atrzPrgrsSttsComCd ?? ""] ?? {
label: "-",
badgeClass: "bg-gray-100 text-gray-700",
}
);
@@ -314,8 +314,8 @@ function ElaprApprovalDialog({
<col width="10%" />
<col width="30%" />
<col width="10%" />
<col width="10%" />
<col width="24%" />
<col width="14%" />
<col width="20%" />
</colgroup>
<TableHeader>
<TableRow>
@@ -4,7 +4,9 @@ import { Fragment } from "react";
import { ChevronRight } from "lucide-react";
import { dayjs, useUserStore } from "@/shared/lib";
import type { ElaprDocPrgrsClientResponseDto } from "@/shared/model";
import { SectionCollapsible } from "@/shared/ui";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
import { cn, Skeleton } from "@common/components";
@@ -18,7 +20,7 @@ interface ElaprDocPrgrsStatusProps {
}
function ElaprDocPrgrsStep({ item }: { item: ElaprDocPrgrsClientResponseDto }) {
const status = getApproverStatus(item.atrzPrgrsSttsComCd);
const status = getApproverStatus(item.atrzPrgrsSttsComCd, item.atrzSeComCd);
const isRejected = item.atrzPrgrsSttsComCd === REJECT_STATUS_CD;
const rjctRsn = item.rjctRsn?.trim();
const metaParts = [item.ognzNm, item.jbpsSeNm].filter(Boolean);
@@ -26,7 +28,7 @@ function ElaprDocPrgrsStep({ item }: { item: ElaprDocPrgrsClientResponseDto }) {
const statusBadge = (
<span
className={cn(
"inline-flex items-center rounded-sm px-2 py-1 text-lg font-medium",
"inline-flex items-center rounded-sm px-4 py-1.5 text-2xl font-semibold",
status.badgeClass,
isRejected && rjctRsn && "cursor-help underline decoration-dotted underline-offset-4",
)}
@@ -36,7 +38,7 @@ function ElaprDocPrgrsStep({ item }: { item: ElaprDocPrgrsClientResponseDto }) {
);
return (
<div className="flex min-w-[12rem] flex-1 flex-col items-center gap-3 p-4 text-center">
<div className="card card-border flex min-w-[12rem] flex-1 flex-col items-center gap-4 border p-6 text-center">
{isRejected && rjctRsn ? (
<Tooltip>
<TooltipTrigger asChild>{statusBadge}</TooltipTrigger>
@@ -49,11 +51,16 @@ function ElaprDocPrgrsStep({ item }: { item: ElaprDocPrgrsClientResponseDto }) {
)}
<div className="flex flex-col items-center gap-1">
<div className="flex items-center gap-2">
<span className="text-xl text-muted-foreground">[{item.atrzSeComNm || "-"}]</span>
<span className="text-xl font-semibold">{item.aprvrNm || item.aprvrUuid || "-"}</span>
<p className="text-2xl font-semibold text-foreground">
<span className="text-muted-foreground">[{item.atrzSeComNm || "-"}]</span>{" "}
{item.aprvrNm || item.aprvrUuid || "-"}
</p>
</div>
{metaParts.length > 0 && (
<span className="text-base text-gray-500">{metaParts.join(" · ")}</span>
<span className="text-lg text-gray-500">{metaParts.join(" · ")}</span>
)}
{item.atrzDt && (
<span className="text-lg text-gray-500">{dayjs(item.atrzDt).format("YYYY-MM-DD")}</span>
)}
</div>
</div>
@@ -61,34 +68,45 @@ function ElaprDocPrgrsStep({ item }: { item: ElaprDocPrgrsClientResponseDto }) {
}
function ElaprDocPrgrsStatus({ elaprDocNo }: ElaprDocPrgrsStatusProps) {
const userInfo = useUserStore((state) => state.userInfo);
const { data, isLoading } = useElaprDocPrgrsByDocNo(elaprDocNo);
const steps = sortByAtrzSeqNo(data ?? []);
if (userInfo?.dtlInstCd !== "ROLE_USER01") {
return null;
}
if (isLoading) {
return (
<div className="flex items-center gap-3 px-4 py-6">
<Skeleton className="h-24 flex-1 rounded-sm" />
<Skeleton className="h-24 flex-1 rounded-sm" />
<Skeleton className="h-24 flex-1 rounded-sm" />
</div>
<SectionCollapsible title="결재현황">
<div className="flex items-center gap-3 px-4 py-6">
<Skeleton className="h-24 flex-1 rounded-sm" />
<Skeleton className="h-24 flex-1 rounded-sm" />
<Skeleton className="h-24 flex-1 rounded-sm" />
</div>
</SectionCollapsible>
);
}
if (steps.length === 0) {
return <div className="py-8 text-center text-xl text-gray-500">결재 정보가 없습니다.</div>;
return null;
}
return (
<div className="flex flex-wrap items-stretch justify-start gap-4 overflow-x-auto">
{steps.map((item, index) => (
<Fragment key={item.elaprPrgrsId ?? `${item.atrzSeqNo}-${item.aprvrUuid ?? index}`}>
<div className="flex items-center">
{index > 0 && <ChevronRight className="size-8 text-muted-foreground" />}
</div>
<ElaprDocPrgrsStep item={item} />
</Fragment>
))}
</div>
<SectionCollapsible title="결재현황">
<div className="flex flex-wrap items-stretch justify-start gap-4 overflow-x-auto p-4">
{steps.map((item, index) => (
<Fragment key={item.elaprPrgrsId ?? `${item.atrzSeqNo}-${item.aprvrUuid ?? index}`}>
{index > 0 && (
<div className="flex items-center">
{<ChevronRight className="size-8 text-muted-foreground" />}
</div>
)}
<ElaprDocPrgrsStep item={item} />
</Fragment>
))}
</div>
</SectionCollapsible>
);
}
@@ -73,14 +73,14 @@ function SortablePrgrsSttsRow({
const isDcarbable = isPrgrsSttsRowDcarbable(row);
const rowDisabled = isLocked && !isEditing;
const atrzSeLabel = row.atrzSeComNm || findCodeLabel(atrzSeCodes, row.atrzSeComCd);
const status = getApproverStatus(row.atrzPrgrsSttsComCd);
const status = getApproverStatus(row.atrzPrgrsSttsComCd, row.atrzSeComCd);
const isRejectedRow = row.atrzPrgrsSttsComCd === "ATRZ_PRGRS_004";
const rjctRsn = row.rjctRsn?.trim();
const statusBadge = (
<span
className={cn(
"inline-flex items-center rounded-sm px-3 py-1.5 text-lg font-semibold",
"inline-flex items-center whitespace-nowrap rounded-sm px-3 py-1.5 text-lg font-semibold",
status.badgeClass,
isRejectedRow && rjctRsn && "cursor-help underline decoration-dotted underline-offset-4",
)}
@@ -92,6 +92,7 @@ interface FnrsCnsltnMngResponseExtension {
mdfcnDt?: string;
sido?: string;
gugun?: string;
techRplyHstryYn?: "Y" | "N";
}
/** 백엔드 FnrsCnsltnMngResponseDto */
@@ -148,6 +148,8 @@ export interface InstlSncnMngResponseDto extends InstlSncnMngSaveRequestDto {
taskSeCdNm: string;
/** 진행상태공통코드명 */
prgrsSttsCdNm: string;
/** 기술검토회신이력여부 */
techRplyHstryYn: "Y" | "N";
/** 재이용시설구분공통코드명 */
rtlztnFcltSeComNm: string;
/** 재이용용도구분공통코드명 */
@@ -1,6 +1,7 @@
import type {
AtflMngClientResponseDto,
CklstRvwRsltTreeDto,
DocSttsChgHstryResponseDto,
EsntlAtchFileMngTreeResponseDto,
PageableParams,
PageResponse,
@@ -61,6 +62,7 @@ export interface SewerBscPlanResponseDto {
rgtr?: string;
rgtrNm?: string;
mdfcnDt?: string;
techRplyHstryYn?: "Y" | "N";
}
/** 백엔드 SewerBscPlanDtlResponseDto */
@@ -199,6 +201,7 @@ export interface SewerBscPlanDetResponseDto {
bizCstList?: SewerBscPlanBizCstResponseDto[];
tmplntList?: SewerBscPlanTmplntResponseDto[];
tmplntTmprStrgList?: SewerBscPlanTmplntTmprStrgResponseDto[];
sttsChgHstryList?: DocSttsChgHstryResponseDto[];
}
/** Controller public 메서드명·시그니처와 동일 */
@@ -11,62 +11,54 @@ import { Badge } from "@common/components";
const columnHelper = createColumnHelper<DesignCnstnMngResponseDto>();
const MAIN_STEP_LABELS = {
save: "저장",
apply: "신청완료",
review: "설계자문 검토",
reply: "설계검토 회신",
apply: "설계자문 신청",
techReview: "기술검토",
reply: "검토결과 회신",
} as const;
const REVIEW_STATUS_CODES = new Set(["CS", "CR"]);
export const PROGRESS_STATUS_COLUMNS = [
{ label: "설계자문 신청 (시군)", sttsComCd: "ER" },
{ label: "기술검토", sttsComCd: "TE" },
{ label: "기술검토 회신", sttsComCd: "CP" },
];
type BadgeState = "completed" | "current" | "pending" | "action";
type BadgeState = "completed" | "current" | "pending";
interface ProgressBadge {
label: string;
state: BadgeState;
}
function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): ProgressBadge[] {
const reviewLabel = prgrsSttsCdNm || MAIN_STEP_LABELS.review;
const replyLabel = prgrsSttsCdNm || MAIN_STEP_LABELS.reply;
function buildProgressBadges(prgrsSttsComCd: string): ProgressBadge[] {
if (prgrsSttsComCd === "CV") {
return [
{ label: MAIN_STEP_LABELS.save, state: "current" },
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
{ label: MAIN_STEP_LABELS.review, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "CT") {
if (prgrsSttsComCd === "ER" || prgrsSttsComCd === "CS" || prgrsSttsComCd === "SL") {
return [
{ label: MAIN_STEP_LABELS.save, state: "completed" },
{ label: MAIN_STEP_LABELS.apply, state: "current" },
{ label: MAIN_STEP_LABELS.review, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (REVIEW_STATUS_CODES.has(prgrsSttsComCd)) {
return [
{ label: MAIN_STEP_LABELS.save, state: "completed" },
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: reviewLabel, state: "action" },
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "CP" || prgrsSttsComCd === "EP") {
if (prgrsSttsComCd === "CP") {
return [
{ label: MAIN_STEP_LABELS.save, state: "completed" },
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.review, state: "completed" },
{ label: replyLabel, state: "action" },
{ label: MAIN_STEP_LABELS.techReview, state: "completed" },
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
];
}
return [{ label: prgrsSttsCdNm || prgrsSttsComCd || "-", state: "current" }];
return [
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
function getBadgeColor(state: BadgeState) {
@@ -77,8 +69,6 @@ function getBadgeColor(state: BadgeState) {
return "blue";
case "pending":
return "lightGray";
case "action":
return "orange";
}
}
@@ -88,11 +78,10 @@ function formatOptional(value: string | undefined) {
interface ProgressStepBadgeProps {
prgrsSttsComCd: string;
prgrsSttsCdNm: string;
}
function ProgressStepBadge({ prgrsSttsComCd, prgrsSttsCdNm }: ProgressStepBadgeProps) {
const badges = buildProgressBadges(prgrsSttsComCd, prgrsSttsCdNm);
function ProgressStepBadge({ prgrsSttsComCd }: ProgressStepBadgeProps) {
const badges = buildProgressBadges(prgrsSttsComCd);
return (
<div className="flex flex-wrap items-center justify-start">
@@ -142,12 +131,7 @@ export function createDesignCnstnColumns() {
id: "progress",
header: "진행상황",
meta: { align: "center" },
cell: ({ row }) => (
<ProgressStepBadge
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
prgrsSttsCdNm={row.original.prgrsSttsCdNm || ""}
/>
),
cell: ({ row }) => <ProgressStepBadge prgrsSttsComCd={row.original.prgrsSttsComCd || ""} />,
}),
columnHelper.accessor("regDt", {
header: "신청일",
@@ -22,6 +22,7 @@ import {
ContactInfoFormSection,
isFormEsntlAtchComplete,
PrivacyConsentSection,
ProgressStatusSection,
} from "@/widgets/pbadms-sprt";
import { Button } from "@common/components";
@@ -31,6 +32,7 @@ import {
formatTaskSeComCdForApi,
} from "../lib/design-cnstn-regist-schema";
import { getCreateDefaultValues, mapDetailToFormData } from "../lib/form-data-mapper";
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
import { BasicInfoFormSection } from "./sections/basic-info-form-section";
@@ -174,6 +176,13 @@ function DesignFormPage({ id, mode, initialData, createDefaults }: DesignFormPag
return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSave)} className="mt-6 space-y-4">
{initialData?.detDto?.designCnstnDocNo && (
<ProgressStatusSection
columns={PROGRESS_STATUS_COLUMNS}
sttsChgHstryList={initialData?.sttsChgHstryList ?? []}
elaprDocNo={initialData.detDto.designCnstnDocNo}
/>
)}
<BasicInfoFormSection />
<ContactInfoFormSection fieldPrefix="designCnstnMng" />
<PrivacyConsentSection />
@@ -6,11 +6,12 @@ import { useUserStore } from "@/shared/lib";
import { useDesignCnstnMng } from "@/entities/design-cnstn";
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import { ProgressStatusSection } from "@/widgets/pbadms-sprt";
import { Skeleton } from "@common/components";
import { needsRoleUser01AtrzResolve } from "../lib/button-config";
import { getEditableReviewStep } from "../lib/review-step-utils";
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
import { FloatingActionButtons } from "./components/floating-action-buttons";
import { AttachmentsSection } from "./sections/attachments-section";
@@ -65,11 +66,14 @@ function DesignViewPage({ id }: DesignViewPageProps) {
return <div className="mt-6 text-[1.4rem]">상세 정보를 찾을 수 없습니다.</div>;
}
const detDto = data.detDto;
const bizSclList = data.bizSclList ?? [];
const esntlFileList = data.esntlFileList ?? [];
const etcFileList = data.etcFileList ?? [];
const rvwList = data.rvwList ?? [];
const {
detDto,
bizSclList = [],
esntlFileList = [],
etcFileList = [],
rvwList = [],
sttsChgHstryList = [],
} = data;
const editableReviewStep = getEditableReviewStep(
userInfo?.dtlInstCd,
@@ -78,11 +82,14 @@ function DesignViewPage({ id }: DesignViewPageProps) {
return (
<div className="mt-6 space-y-4">
<ProgressStatusSection
columns={PROGRESS_STATUS_COLUMNS}
sttsChgHstryList={sttsChgHstryList}
elaprDocNo={detDto.designCnstnDocNo}
/>
<BasicInfoSection det={detDto} />
<ContactInfoSection det={detDto} />
<SectionCollapsible title="결재현황">
<ElaprDocPrgrsStatus elaprDocNo={detDto.designCnstnDocNo} />
</SectionCollapsible>
<ElaprDocPrgrsStatus elaprDocNo={detDto.designCnstnDocNo} />
<BusinessScaleSection bizSclList={bizSclList} />
<AttachmentsSection esntlFileList={esntlFileList} etcFileList={etcFileList} />
<TechReviewOpinionSection
@@ -1,12 +1,12 @@
"use client";
import { formatFileSize } from "@/shared/lib/file-utils";
import { SectionCollapsible } from "@/shared/ui";
import { useDownloadAttachmentByAtflCd } from "@/entities/attach";
import type {
AtflMngClientResponseDto,
EsntlAtchFileMngTreeResponseDto,
} from "@/entities/design-cnstn";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface AttachmentsSectionProps {
@@ -3,10 +3,10 @@
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldError, FieldGroup } from "@/shared/ui/field";
import { findCodeLabel, useCodeList } from "@/entities/code";
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import {
Button,
Input,
@@ -1,7 +1,7 @@
"use client";
import { SectionCollapsible } from "@/shared/ui";
import type { DesignCnstnMngResponseDto } from "@/entities/design-cnstn";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface BasicInfoSectionProps {
@@ -1,7 +1,8 @@
"use client";
import { SectionCollapsible } from "@/shared/ui";
import type { BizSclTreeDto } from "@/entities/design-cnstn";
import { fmtNumber, indentPx, SectionCollapsible } from "@/widgets/pbadms-sprt";
import { fmtNumber, indentPx } from "@/widgets/pbadms-sprt";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface BusinessScaleSectionProps {
@@ -1,7 +1,7 @@
"use client";
import { SectionCollapsible } from "@/shared/ui";
import type { DesignCnstnMngResponseDto } from "@/entities/design-cnstn";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface ContactInfoSectionProps {
@@ -7,10 +7,10 @@ import { Paperclip } from "lucide-react";
import { PROXY_HOST } from "@/shared/config";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
import { attachService } from "@/entities/attach";
import type { DesignCnstnRvwOpnnResponseDto } from "@/entities/design-cnstn";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import {
Button,
RadioGroup,
+53 -31
View File
@@ -11,29 +11,33 @@ import { Badge } from "@common/components";
const columnHelper = createColumnHelper<FnrsCnsltnMngResponseDto>();
const MAIN_STEP_LABELS = {
save: "저장",
apply: "신청완료",
apply: "재원협의 신청",
envoReview: "환경청 검토",
techReview: "기술검토",
reply: "검토결과 회신",
} as const;
const TECH_REVIEW_STATUS_CODES = new Set(["LE", "ER", "EE", "TE", "TS", "TR", "TP"]);
export const PROGRESS_STATUS_COLUMNS = [
{ label: "재원협의 신청 (시군)", sttsComCd: "ST" },
{ label: "재원협의 신청 (시도)", sttsComCd: "LE" },
{ label: "환경청 검토", sttsComCd: "ER" },
{ label: "기술검토", sttsComCd: "TE" },
{ label: "기술검토 회신", sttsComCd: "TP" },
{ label: "환경청 검토 결과 회신", sttsComCd: "EP" },
];
type BadgeState = "completed" | "current" | "pending" | "action";
type BadgeState = "completed" | "current" | "pending";
interface ProgressBadge {
label: string;
state: BadgeState;
}
function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): ProgressBadge[] {
const techReviewLabel = prgrsSttsCdNm || MAIN_STEP_LABELS.techReview;
const replyLabel = prgrsSttsCdNm || MAIN_STEP_LABELS.reply;
function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N"): ProgressBadge[] {
if (prgrsSttsComCd === "SV") {
return [
{ label: MAIN_STEP_LABELS.save, state: "current" },
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
@@ -41,41 +45,61 @@ function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): Pro
if (prgrsSttsComCd === "ST") {
return [
{ label: MAIN_STEP_LABELS.save, state: "completed" },
{ label: MAIN_STEP_LABELS.apply, state: "current" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (TECH_REVIEW_STATUS_CODES.has(prgrsSttsComCd)) {
if (prgrsSttsComCd === "LE") {
return [
{ label: MAIN_STEP_LABELS.save, state: "completed" },
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: techReviewLabel, state: "action" },
{ label: MAIN_STEP_LABELS.envoReview, state: "current" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "ER" || prgrsSttsComCd === "TE" || prgrsSttsComCd === "SL") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "TP") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{
label: MAIN_STEP_LABELS.techReview,
state: techRplyHstryYn === "Y" ? "completed" : "pending",
},
{ label: MAIN_STEP_LABELS.reply, state: "current" },
];
}
if (prgrsSttsComCd === "EP") {
return [
{ label: MAIN_STEP_LABELS.save, state: "completed" },
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.techReview, state: "completed" },
{ label: replyLabel, state: "action" },
];
}
if (prgrsSttsComCd === "TA" || prgrsSttsComCd === "TB") {
return [
{ label: MAIN_STEP_LABELS.save, state: "completed" },
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.techReview, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{
label: MAIN_STEP_LABELS.techReview,
state: techRplyHstryYn === "Y" ? "completed" : "pending",
},
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
];
}
return [{ label: prgrsSttsCdNm || prgrsSttsComCd || "-", state: "current" }];
return [
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
function getBadgeColor(state: BadgeState) {
@@ -86,8 +110,6 @@ function getBadgeColor(state: BadgeState) {
return "blue";
case "pending":
return "lightGray";
case "action":
return "orange";
}
}
@@ -97,11 +119,11 @@ function formatOptional(value: string | undefined) {
interface ProgressStepBadgeProps {
prgrsSttsComCd: string;
prgrsSttsCdNm: string;
techRplyHstryYn: "Y" | "N";
}
function ProgressStepBadge({ prgrsSttsComCd, prgrsSttsCdNm }: ProgressStepBadgeProps) {
const badges = buildProgressBadges(prgrsSttsComCd, prgrsSttsCdNm);
function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadgeProps) {
const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn);
return (
<div className="flex flex-wrap items-center justify-start">
@@ -154,7 +176,7 @@ export function createFnrsCnsltnColumns() {
cell: ({ row }) => (
<ProgressStepBadge
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
prgrsSttsCdNm={row.original.prgrsSttsCdNm || ""}
techRplyHstryYn={row.original.techRplyHstryYn || "N"}
/>
),
}),
@@ -30,6 +30,7 @@ import {
isFormChecklistComplete,
isFormEsntlAtchComplete,
PrivacyConsentSection,
ProgressStatusSection,
recalculateFnrsSharTotals,
SplmntDmndFormSection,
toFnrsSharSaveList,
@@ -43,6 +44,7 @@ import {
formatTaskSeComCdForApi,
} from "../lib/fnrs-cnsltn-regist-schema";
import { getCreateDefaultValues, mapDetailToFormData } from "../lib/form-data-mapper";
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
import { BasicInfoFormSection } from "./sections/basic-info-form-section";
interface FnrsFormPageProps {
@@ -270,6 +272,13 @@ function FnrsFormPage({ id, mode, initialData, createDefaults }: FnrsFormPagePro
return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSave)} className="mt-6 space-y-4">
{initialData?.detDto?.fnrsCnsltnDocNo && (
<ProgressStatusSection
columns={PROGRESS_STATUS_COLUMNS}
sttsChgHstryList={initialData?.sttsChgHstryList ?? []}
elaprDocNo={initialData.detDto.fnrsCnsltnDocNo}
/>
)}
<BasicInfoFormSection />
<ContactInfoFormSection fieldPrefix="fnrsCnsltnMng" />
<PrivacyConsentSection />
@@ -6,12 +6,13 @@ import { useUserStore } from "@/shared/lib";
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
import { useFnrsCnsltnMng } from "@/entities/fnrs-cnsltn";
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
import { SectionCollapsible, SupplementDemandSection } from "@/widgets/pbadms-sprt";
import { ProgressStatusSection, SupplementDemandSection } from "@/widgets/pbadms-sprt";
import { Skeleton } from "@common/components";
import { needsRoleUser01AtrzResolve } from "../lib/button-config";
import { getEditableReviewStep } from "../lib/review-step-utils";
import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-checklist-form-state";
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
import { FloatingActionButtons } from "./components/floating-action-buttons";
import { AttachmentsSection } from "./sections/attachments-section";
@@ -71,15 +72,18 @@ function FnrsViewPage({ id }: FnrsViewPageProps) {
return <div className="mt-6 text-[1.4rem]">상세 정보를 찾을 수 없습니다.</div>;
}
const detDto = data.detDto;
const bizSclList = data.bizSclList ?? [];
const bizCstList = data.bizCstList ?? [];
const cklstResultList = data.cklstResultList ?? [];
const esntlFileList = data.esntlFileList ?? [];
const etcFileList = data.etcFileList ?? [];
const rvwList = data.rvwList ?? [];
const splmntDmndList = data.splmntDmndList ?? [];
const fnrsSharList = data.fnrsSharList ?? [];
const {
detDto,
bizSclList = [],
bizCstList = [],
cklstResultList = [],
esntlFileList = [],
etcFileList = [],
rvwList = [],
splmntDmndList = [],
fnrsSharList = [],
sttsChgHstryList = [],
} = data;
const editableReviewStep = getEditableReviewStep(
userInfo?.dtlInstCd,
@@ -88,11 +92,14 @@ function FnrsViewPage({ id }: FnrsViewPageProps) {
return (
<div className="mt-6 space-y-4">
<ProgressStatusSection
columns={PROGRESS_STATUS_COLUMNS}
sttsChgHstryList={sttsChgHstryList}
elaprDocNo={detDto.fnrsCnsltnDocNo}
/>
<BasicInfoSection det={detDto} />
<ContactInfoSection det={detDto} />
<SectionCollapsible title="결재현황">
<ElaprDocPrgrsStatus elaprDocNo={detDto.fnrsCnsltnDocNo} />
</SectionCollapsible>
<ElaprDocPrgrsStatus elaprDocNo={detDto.fnrsCnsltnDocNo} />
<BusinessScaleSection bizSclList={bizSclList} />
<BusinessCostSection bizCstList={bizCstList} />
<FnrsSharSection fnrsSharList={fnrsSharList} />
@@ -1,12 +1,12 @@
"use client";
import { formatFileSize } from "@/shared/lib/file-utils";
import { SectionCollapsible } from "@/shared/ui";
import { useDownloadAttachmentByAtflCd } from "@/entities/attach";
import type {
AtflMngClientResponseDto,
EsntlAtchFileMngTreeResponseDto,
} from "@/entities/fnrs-cnsltn";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface AttachmentsSectionProps {
@@ -3,10 +3,10 @@
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from "@/shared/ui/field";
import { findCodeLabel, useCodeList } from "@/entities/code";
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import {
Button,
Checkbox,
@@ -1,7 +1,7 @@
"use client";
import { SectionCollapsible } from "@/shared/ui";
import type { FnrsCnsltnMngResponseDto } from "@/entities/fnrs-cnsltn";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface BasicInfoSectionProps {
@@ -2,9 +2,9 @@
import { useMemo } from "react";
import { SectionCollapsible } from "@/shared/ui";
import type { BizCstTreeDto } from "@/entities/fnrs-cnsltn";
import { calcDiffPercent, fmtNumber, indentPx } from "@/widgets/pbadms-sprt";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
import { sortBizCstListByTreePath } from "../../lib/business-cost-utils";
@@ -1,8 +1,8 @@
"use client";
import { SectionCollapsible } from "@/shared/ui";
import type { BizSclTreeDto } from "@/entities/fnrs-cnsltn";
import { fmtNumber, indentPx } from "@/widgets/pbadms-sprt";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface BusinessScaleSectionProps {
@@ -2,8 +2,8 @@
import { type Dispatch, type SetStateAction, useEffect, useMemo, useState } from "react";
import { SectionCollapsible } from "@/shared/ui";
import type { CklstRvwRsltTreeDto } from "@/entities/fnrs-cnsltn";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import {
@@ -1,7 +1,7 @@
"use client";
import { SectionCollapsible } from "@/shared/ui";
import type { FnrsCnsltnMngResponseDto } from "@/entities/fnrs-cnsltn";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface ContactInfoSectionProps {
@@ -2,8 +2,9 @@
import { useMemo } from "react";
import { SectionCollapsible } from "@/shared/ui";
import type { FnrsSharBizCstTreeDto } from "@/entities/fnrs-shar-se";
import { fmtNumber, indentPx, SectionCollapsible } from "@/widgets/pbadms-sprt";
import { fmtNumber, indentPx } from "@/widgets/pbadms-sprt";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface FnrsSharSectionProps {
@@ -7,10 +7,10 @@ import { Paperclip } from "lucide-react";
import { PROXY_HOST } from "@/shared/config";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
import { attachService } from "@/entities/attach";
import type { FnrsCnsltnRvwOpnnResponseDto } from "@/entities/fnrs-cnsltn";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import {
Button,
RadioGroup,
+49 -26
View File
@@ -11,29 +11,34 @@ import { Badge } from "@common/components";
const columnHelper = createColumnHelper<InstlSncnMngResponseDto>();
const MAIN_STEP_LABELS = {
save: "저장",
apply: "신청완료",
apply: "설치인가 신청",
envoReview: "환경청 검토",
techReview: "기술검토",
reply: "검토결과 회신",
} as const;
const TECH_REVIEW_STATUS_CODES = new Set(["LE", "ER", "EE", "TE", "TS", "TR", "TP"]);
export const PROGRESS_STATUS_COLUMNS = [
{ label: "설치인가 신청 (시군)", sttsComCd: "ST" },
{ label: "설치인가 신청 (시도)", sttsComCd: "LE" },
{ label: "환경청 검토", sttsComCd: "ER" },
{ label: "기술검토", sttsComCd: "TE" },
{ label: "기술검토 회신", sttsComCd: "TP" },
{ label: "검토결과 회신", sttsComCd: "EP" },
{ label: "설치인가 승인", sttsComCd: "TA" },
];
type BadgeState = "completed" | "current" | "pending" | "action";
type BadgeState = "completed" | "current" | "pending";
interface ProgressBadge {
label: string;
state: BadgeState;
}
function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): ProgressBadge[] {
const techReviewLabel = prgrsSttsCdNm || MAIN_STEP_LABELS.techReview;
const replyLabel = prgrsSttsCdNm || MAIN_STEP_LABELS.reply;
function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N"): ProgressBadge[] {
if (prgrsSttsComCd === "SV") {
return [
{ label: MAIN_STEP_LABELS.save, state: "current" },
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
@@ -41,41 +46,61 @@ function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): Pro
if (prgrsSttsComCd === "ST") {
return [
{ label: MAIN_STEP_LABELS.save, state: "completed" },
{ label: MAIN_STEP_LABELS.apply, state: "current" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (TECH_REVIEW_STATUS_CODES.has(prgrsSttsComCd)) {
if (prgrsSttsComCd === "LE") {
return [
{ label: MAIN_STEP_LABELS.save, state: "completed" },
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: techReviewLabel, state: "action" },
{ label: MAIN_STEP_LABELS.envoReview, state: "current" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "EP") {
if (prgrsSttsComCd === "ER" || prgrsSttsComCd === "TE" || prgrsSttsComCd === "SL") {
return [
{ label: MAIN_STEP_LABELS.save, state: "completed" },
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.techReview, state: "completed" },
{ label: replyLabel, state: "action" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "TP" || prgrsSttsComCd === "EP") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{
label: MAIN_STEP_LABELS.techReview,
state: techRplyHstryYn === "Y" ? "completed" : "pending",
},
{ label: MAIN_STEP_LABELS.reply, state: "current" },
];
}
if (prgrsSttsComCd === "TA" || prgrsSttsComCd === "TB") {
return [
{ label: MAIN_STEP_LABELS.save, state: "completed" },
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.techReview, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{
label: MAIN_STEP_LABELS.techReview,
state: techRplyHstryYn === "Y" ? "completed" : "pending",
},
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
];
}
return [{ label: prgrsSttsCdNm || prgrsSttsComCd || "-", state: "current" }];
return [
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
function getBadgeColor(state: BadgeState) {
@@ -86,8 +111,6 @@ function getBadgeColor(state: BadgeState) {
return "blue";
case "pending":
return "lightGray";
case "action":
return "orange";
}
}
@@ -97,11 +120,11 @@ function formatOptional(value: string | undefined) {
interface ProgressStepBadgeProps {
prgrsSttsComCd: string;
prgrsSttsCdNm: string;
techRplyHstryYn: "Y" | "N";
}
function ProgressStepBadge({ prgrsSttsComCd, prgrsSttsCdNm }: ProgressStepBadgeProps) {
const badges = buildProgressBadges(prgrsSttsComCd, prgrsSttsCdNm);
function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadgeProps) {
const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn);
return (
<div className="flex flex-wrap items-center justify-start">
@@ -154,7 +177,7 @@ export function createInstlSncnColumns() {
cell: ({ row }) => (
<ProgressStepBadge
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
prgrsSttsCdNm={row.original.prgrsSttsCdNm || ""}
techRplyHstryYn={row.original.techRplyHstryYn || "N"}
/>
),
}),
@@ -25,6 +25,7 @@ import {
isFormChecklistComplete,
isFormEsntlAtchComplete,
PrivacyConsentSection,
ProgressStatusSection,
SplmntDmndFormSection,
} from "@/widgets/pbadms-sprt";
import { Button } from "@common/components";
@@ -35,6 +36,7 @@ import {
type InstlSncnRegistFormData,
instlSncnRegistSchema,
} from "../lib/instl-sncn-regist-schema";
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
import { BasicInfoFormSection } from "./sections/basic-info-form-section";
interface InstlFormPageProps {
@@ -228,6 +230,13 @@ function InstlFormPage({ id, mode, initialData, createDefaults }: InstlFormPageP
return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSave)} className="mt-6 space-y-4">
{initialData?.detDto.instlSncnDocNo && (
<ProgressStatusSection
columns={PROGRESS_STATUS_COLUMNS}
sttsChgHstryList={initialData?.sttsChgHstryList ?? []}
elaprDocNo={initialData.detDto.instlSncnDocNo}
/>
)}
<BasicInfoFormSection />
<ContactInfoFormSection fieldPrefix="instlSncnMng" />
<PrivacyConsentSection />
@@ -6,12 +6,13 @@ import { useUserStore } from "@/shared/lib";
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
import { useInstlSncnDetail } from "@/entities/instl-sncn";
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
import { SectionCollapsible, SupplementDemandSection } from "@/widgets/pbadms-sprt";
import { ProgressStatusSection, SupplementDemandSection } from "@/widgets/pbadms-sprt";
import { Skeleton } from "@common/components";
import { needsRoleUser01AtrzResolve } from "../lib/button-config";
import { getEditableReviewStep } from "../lib/review-step-utils";
import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-checklist-form-state";
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
import { FloatingActionButtons } from "./components/floating-action-buttons";
import { AttachmentsSection } from "./sections/attachments-section";
@@ -79,6 +80,7 @@ function InstlViewPage({ id }: InstlViewPageProps) {
etcFileList,
rvwList = [],
splmntDmndList = [],
sttsChgHstryList = [],
} = data;
const editableReviewStep = getEditableReviewStep(
@@ -88,11 +90,14 @@ function InstlViewPage({ id }: InstlViewPageProps) {
return (
<div className="mt-6 space-y-4">
<ProgressStatusSection
columns={PROGRESS_STATUS_COLUMNS}
sttsChgHstryList={sttsChgHstryList}
elaprDocNo={detDto.instlSncnDocNo}
/>
<BasicInfoSection det={detDto} />
<ContactInfoSection det={detDto} />
<SectionCollapsible title="결재현황">
<ElaprDocPrgrsStatus elaprDocNo={detDto.instlSncnDocNo} />
</SectionCollapsible>
<ElaprDocPrgrsStatus elaprDocNo={detDto.instlSncnDocNo} />
<BusinessScaleSection bizSclList={bizSclList} />
<BusinessCostSection bizCstList={bizCstList} />
<ChecklistSection
@@ -1,12 +1,12 @@
"use client";
import { formatFileSize } from "@/shared/lib/file-utils";
import { SectionCollapsible } from "@/shared/ui";
import { useDownloadAttachmentByAtflCd } from "@/entities/attach";
import type {
AtflMngClientResponseDto,
EsntlAtchFileMngTreeResponseDto,
} from "@/entities/instl-sncn";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface AttachmentsSectionProps {
@@ -3,10 +3,10 @@
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from "@/shared/ui/field";
import { findCodeLabel, useCodeList } from "@/entities/code";
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import {
Button,
Checkbox,
@@ -1,7 +1,7 @@
"use client";
import { SectionCollapsible } from "@/shared/ui";
import type { InstlSncnMngResponseDto } from "@/entities/instl-sncn";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface BasicInfoSectionProps {
@@ -2,9 +2,9 @@
import { useMemo } from "react";
import { SectionCollapsible } from "@/shared/ui";
import type { BizCstTreeDto } from "@/entities/instl-sncn";
import { calcDiffPercent, fmtNumber, indentPx } from "@/widgets/pbadms-sprt";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
import { sortBizCstListByTreePath } from "../../lib/business-cost-utils";
@@ -1,8 +1,8 @@
"use client";
import { SectionCollapsible } from "@/shared/ui";
import type { BizSclTreeDto } from "@/entities/instl-sncn";
import { fmtNumber, indentPx } from "@/widgets/pbadms-sprt";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface BusinessScaleSectionProps {
@@ -2,8 +2,8 @@
import { type Dispatch, type SetStateAction, useEffect, useMemo, useState } from "react";
import { SectionCollapsible } from "@/shared/ui";
import type { CklstRvwRsltTreeDto, ReviewStepCode } from "@/entities/instl-sncn";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import {
@@ -1,7 +1,7 @@
"use client";
import { SectionCollapsible } from "@/shared/ui";
import type { InstlSncnMngResponseDto } from "@/entities/instl-sncn";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface ContactInfoSectionProps {
@@ -7,10 +7,10 @@ import { Paperclip } from "lucide-react";
import { PROXY_HOST } from "@/shared/config";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
import { attachService } from "@/entities/attach";
import type { InstlSncnRvwOpnnResponseDto, ReviewStepCode } from "@/entities/instl-sncn";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import {
Button,
RadioGroup,
@@ -11,30 +11,30 @@ import { Badge } from "@common/components";
const columnHelper = createColumnHelper<SewerBscPlanResponseDto>();
const MAIN_STEP_LABELS = {
save: "저장",
apply: "신청완료",
apply: "기본계획 신청",
envoReview: "환경청 검토",
techReview: "기술검토",
reply: "검토결과 회신",
supplement: "보완요청",
} as const;
const TECH_REVIEW_STATUS_CODES = new Set(["ER", "TE", "TP"]);
export const PROGRESS_STATUS_COLUMNS = [
{ label: "기본계획 신청 (시군)", sttsComCd: "LE" },
{ label: "환경청 검토", sttsComCd: "ER" },
{ label: "기술검토", sttsComCd: "TE" },
{ label: "기술검토 회신", sttsComCd: "TP" },
{ label: "횐경청 검토 결과 회신", sttsComCd: "EP" },
];
type BadgeState = "completed" | "current" | "pending" | "action";
type BadgeState = "completed" | "current" | "pending";
interface ProgressBadge {
label: string;
state: BadgeState;
}
function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): ProgressBadge[] {
const techReviewLabel = prgrsSttsCdNm || MAIN_STEP_LABELS.techReview;
const replyLabel = prgrsSttsCdNm || MAIN_STEP_LABELS.reply;
function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N"): ProgressBadge[] {
if (prgrsSttsComCd === "SV") {
return [
{ label: MAIN_STEP_LABELS.save, state: "current" },
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
@@ -42,57 +42,63 @@ function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): Pro
];
}
if (prgrsSttsComCd === "ST") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "current" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "LE") {
return [
{ label: MAIN_STEP_LABELS.save, state: "completed" },
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "action" },
{ label: MAIN_STEP_LABELS.envoReview, state: "current" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "SL") {
if (prgrsSttsComCd === "ER" || prgrsSttsComCd === "TE" || prgrsSttsComCd === "SL") {
return [
{ label: MAIN_STEP_LABELS.save, state: "completed" },
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{ label: MAIN_STEP_LABELS.supplement, state: "action" },
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (TECH_REVIEW_STATUS_CODES.has(prgrsSttsComCd)) {
if (prgrsSttsComCd === "TP") {
return [
{ label: MAIN_STEP_LABELS.save, state: "completed" },
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{ label: techReviewLabel, state: "action" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "TA") {
return [
{ label: MAIN_STEP_LABELS.save, state: "completed" },
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{ label: MAIN_STEP_LABELS.techReview, state: "completed" },
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
{
label: MAIN_STEP_LABELS.techReview,
state: techRplyHstryYn === "Y" ? "completed" : "pending",
},
{ label: MAIN_STEP_LABELS.reply, state: "current" },
];
}
if (prgrsSttsComCd === "EP") {
return [
{ label: MAIN_STEP_LABELS.save, state: "completed" },
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{ label: MAIN_STEP_LABELS.techReview, state: "completed" },
{ label: replyLabel, state: "completed" },
{
label: MAIN_STEP_LABELS.techReview,
state: techRplyHstryYn === "Y" ? "completed" : "pending",
},
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
];
}
return [{ label: prgrsSttsCdNm || prgrsSttsComCd || "-", state: "current" }];
return [
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
function getBadgeColor(state: BadgeState) {
@@ -103,8 +109,6 @@ function getBadgeColor(state: BadgeState) {
return "blue";
case "pending":
return "lightGray";
case "action":
return "orange";
}
}
@@ -114,11 +118,11 @@ function formatOptional(value: string | undefined) {
interface ProgressStepBadgeProps {
prgrsSttsComCd: string;
prgrsSttsCdNm: string;
techRplyHstryYn: "Y" | "N";
}
function ProgressStepBadge({ prgrsSttsComCd, prgrsSttsCdNm }: ProgressStepBadgeProps) {
const badges = buildProgressBadges(prgrsSttsComCd, prgrsSttsCdNm);
function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadgeProps) {
const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn);
return (
<div className="flex flex-wrap items-center justify-start">
@@ -177,7 +181,7 @@ export function createSewerBscPlanColumns() {
cell: ({ row }) => (
<ProgressStepBadge
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
prgrsSttsCdNm={row.original.prgrsSttsCdNm || ""}
techRplyHstryYn={row.original.techRplyHstryYn || "N"}
/>
),
}),
@@ -3,13 +3,13 @@
import { useEffect, useMemo, useState } from "react";
import { useAlertStore } from "@/shared/lib";
import { SectionCollapsible } from "@/shared/ui";
import {
type CmCdAdstDstrComboResponseDto,
useFindGugunCombo,
useFindSidoCombo,
} from "@/entities/adstdstr";
import { useLvNowDmgMtncGnsvy, useNowDmgMtncGnsvy } from "@/entities/dc";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import {
Button,
Dialog,
@@ -3,12 +3,12 @@
import { Paperclip } from "lucide-react";
import { formatFileSize } from "@/shared/lib/file-utils";
import { SectionCollapsible } from "@/shared/ui";
import { useDownloadAttachmentByAtflCd } from "@/entities/attach";
import type {
AtflMngClientResponseDto,
EsntlAtchFileMngTreeResponseDto,
} from "@/entities/sewer-bsc-plan";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface AttachmentsSectionProps {
@@ -3,8 +3,8 @@
import { Controller, useFormContext } from "react-hook-form";
import { useUserStore } from "@/shared/lib";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldError } from "@/shared/ui/field";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import {
Input,
Select,
@@ -1,10 +1,10 @@
"use client";
import { SectionCollapsible } from "@/shared/ui";
import type {
SewerBscPlanDtlResponseDto,
SewerBscPlanResponseDto,
} from "@/entities/sewer-bsc-plan";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface BasicInfoSectionProps {
@@ -3,9 +3,10 @@
import { useEffect, useMemo } from "react";
import { useFormContext, useWatch } from "react-hook-form";
import { SectionCollapsible } from "@/shared/ui";
import { Field } from "@/shared/ui/field";
import { useAcntgSeMngTree } from "@/entities/acntg";
import { buildAcntgCostItems, SectionCollapsible } from "@/widgets/pbadms-sprt";
import { buildAcntgCostItems } from "@/widgets/pbadms-sprt";
import {
Input,
Skeleton,
@@ -2,12 +2,13 @@
import { useMemo } from "react";
import { SectionCollapsible } from "@/shared/ui";
import { useAcntgSeMngTree } from "@/entities/acntg";
import type {
SewerBscPlanBizCstResponseDto,
SewerBscPlanDtlResponseDto,
} from "@/entities/sewer-bsc-plan";
import { buildAcntgCostItems, fmtNumber, SectionCollapsible } from "@/widgets/pbadms-sprt";
import { buildAcntgCostItems, fmtNumber } from "@/widgets/pbadms-sprt";
import {
Skeleton,
Table,
@@ -2,8 +2,8 @@
import { type Dispatch, type SetStateAction, useEffect, useMemo, useState } from "react";
import { SectionCollapsible } from "@/shared/ui";
import type { CklstRvwRsltTreeDto } from "@/entities/sewer-bsc-plan";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import {
@@ -1,7 +1,7 @@
"use client";
import { SectionCollapsible } from "@/shared/ui";
import type { SewerBscPlanDtlResponseDto } from "@/entities/sewer-bsc-plan";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface ContactInfoSectionProps {
@@ -3,9 +3,10 @@
import { useMemo } from "react";
import { useFormContext, useWatch } from "react-hook-form";
import { SectionCollapsible } from "@/shared/ui";
import { Field } from "@/shared/ui/field";
import { useAcntgSeMngTree } from "@/entities/acntg";
import { buildAcntgCostItems, SectionCollapsible } from "@/widgets/pbadms-sprt";
import { buildAcntgCostItems } from "@/widgets/pbadms-sprt";
import {
cn,
Input,
@@ -2,12 +2,13 @@
import { useMemo } from "react";
import { SectionCollapsible } from "@/shared/ui";
import { useAcntgSeMngTree } from "@/entities/acntg";
import type {
SewerBscPlanDtlResponseDto,
SewerBscPlanFnrsCstResponseDto,
} from "@/entities/sewer-bsc-plan";
import { buildAcntgCostItems, fmtNumber, SectionCollapsible } from "@/widgets/pbadms-sprt";
import { buildAcntgCostItems, fmtNumber } from "@/widgets/pbadms-sprt";
import {
Skeleton,
Table,
@@ -4,8 +4,8 @@ import { useState } from "react";
import { useFormContext, useWatch } from "react-hook-form";
import dynamic from "next/dynamic";
import { SectionCollapsible } from "@/shared/ui";
import { Field } from "@/shared/ui/field";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import {
Button,
Input,
@@ -1,10 +1,11 @@
"use client";
import { SectionCollapsible } from "@/shared/ui";
import type {
SewerBscPlanDtlResponseDto,
SewerBscPlanOtlnResponseDto,
} from "@/entities/sewer-bsc-plan";
import { fmtNumber, SectionCollapsible } from "@/widgets/pbadms-sprt";
import { fmtNumber } from "@/widgets/pbadms-sprt";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface OutlineSectionProps {
@@ -7,10 +7,10 @@ import { Paperclip } from "lucide-react";
import { PROXY_HOST } from "@/shared/config";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
import { attachService } from "@/entities/attach";
import type { SewerBscPlanRvwOpnnResponseDto } from "@/entities/sewer-bsc-plan";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import {
Button,
RadioGroup,
@@ -7,11 +7,11 @@ import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { getDefaultFileKey, useTus } from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { Field } from "@/shared/ui/field";
import { attachService } from "@/entities/attach";
import { useFacilityList } from "@/entities/bns-if";
import { useNowDmgMtncGnsvy } from "@/entities/dc";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import {
Button,
Checkbox,
@@ -4,6 +4,7 @@ import { useMemo, useState } from "react";
import { Paperclip } from "lucide-react";
import { SectionCollapsible } from "@/shared/ui";
import { useDownloadAttachmentByAtflCd } from "@/entities/attach";
import { useFacilityList } from "@/entities/bns-if";
import { useNowDmgMtncGnsvy } from "@/entities/dc";
@@ -12,7 +13,7 @@ import type {
SewerBscPlanResponseDto,
SewerBscPlanTmplntResponseDto,
} from "@/entities/sewer-bsc-plan";
import { fmtNumber, SectionCollapsible } from "@/widgets/pbadms-sprt";
import { fmtNumber } from "@/widgets/pbadms-sprt";
import {
Table,
TableBody,
@@ -25,6 +25,7 @@ import {
isFormChecklistComplete,
isFormEsntlAtchComplete,
PrivacyConsentSection,
ProgressStatusSection,
SplmntDmndFormSection,
} from "@/widgets/pbadms-sprt";
import { Button } from "@common/components";
@@ -39,6 +40,7 @@ import {
type SewerBscPlanRegistFormData,
sewerBscPlanRegistSchema,
} from "../lib/sewer-bsc-plan-regist-schema";
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
import { BasicInfoFormSection } from "./sections/basic-info-form-section";
import { BizCstMatrixFormSection } from "./sections/biz-cst-matrix-form-section";
@@ -242,6 +244,13 @@ function SewerBscPlanFormPage({
return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSave)} className="mt-6 space-y-4">
{initialData?.detDto?.sewerBscPlanDocNo && (
<ProgressStatusSection
columns={PROGRESS_STATUS_COLUMNS}
sttsChgHstryList={initialData?.sttsChgHstryList ?? []}
elaprDocNo={initialData.detDto.sewerBscPlanDocNo}
/>
)}
<BasicInfoFormSection />
<ContactInfoFormSection fieldPrefix="sewerBscPlanDtl" />
<PrivacyConsentSection />
@@ -6,12 +6,13 @@ import { useUserStore } from "@/shared/lib";
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
import { useSewerBscPlanDetails } from "@/entities/sewer-bsc-plan";
import { SectionCollapsible, SupplementDemandSection } from "@/widgets/pbadms-sprt";
import { ProgressStatusSection, SupplementDemandSection } from "@/widgets/pbadms-sprt";
import { Skeleton } from "@common/components";
import { needsRoleUser01AtrzResolve } from "../lib/button-config";
import { getEditableReviewStep } from "../lib/review-step-utils";
import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-checklist-form-state";
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
import { FloatingActionButtons } from "./components/floating-action-buttons";
import { AttachmentsSection } from "./sections/attachments-section";
@@ -74,17 +75,20 @@ function SewerBscPlanViewPage({ id, propChgCycl }: SewerBscPlanViewPageProps) {
return <div className="mt-6 text-[1.4rem]">상세 정보를 찾을 수 없습니다.</div>;
}
const detDto = data.detDto;
const activeDtlDto = data.activeDtlDto;
const otlnList = data.otlnList ?? [];
const bizCstList = data.bizCstList ?? [];
const fnrsCstList = data.fnrsCstList ?? [];
const tmplntList = data.tmplntList ?? [];
const cklstResultRows = data.cklstResultRows ?? [];
const esntlFileList = data.esntlFileList ?? [];
const etcFileList = data.etcFileList ?? [];
const rvwOpnnList = data.rvwOpnnList ?? [];
const splmntDmndList = data.splmntDmndList ?? [];
const {
detDto,
activeDtlDto,
otlnList = [],
bizCstList = [],
fnrsCstList = [],
tmplntList = [],
cklstResultRows = [],
esntlFileList = [],
etcFileList = [],
rvwOpnnList = [],
splmntDmndList = [],
sttsChgHstryList = [],
} = data;
const editableReviewStep = getEditableReviewStep(
userInfo?.dtlInstCd,
@@ -93,11 +97,14 @@ function SewerBscPlanViewPage({ id, propChgCycl }: SewerBscPlanViewPageProps) {
return (
<div className="mt-6 space-y-4">
<ProgressStatusSection
columns={PROGRESS_STATUS_COLUMNS}
sttsChgHstryList={sttsChgHstryList}
elaprDocNo={detDto.sewerBscPlanDocNo}
/>
<BasicInfoSection det={detDto} dtl={activeDtlDto} />
<ContactInfoSection dtl={activeDtlDto} />
<SectionCollapsible title="결재현황">
<ElaprDocPrgrsStatus elaprDocNo={detDto.sewerBscPlanDocNo} />
</SectionCollapsible>
<ElaprDocPrgrsStatus elaprDocNo={detDto.sewerBscPlanDocNo} />
<OutlineSection otlnList={otlnList} dtl={activeDtlDto} />
<BizCstMatrixSection bizCstList={bizCstList} dtl={activeDtlDto} />
<FnrsCstSection fnrsCstList={fnrsCstList} dtl={activeDtlDto} />
+1
View File
@@ -38,6 +38,7 @@ export type {
CklstResultSaveBase,
CklstRvwRsltTreeDto,
DocPrgrsUpdateRequestDto,
DocSttsChgHstryResponseDto,
ElaprDocPrgrsClientResponseDto,
EsntlAtchFileMngTreeResponseDto,
EsntlAtchFileSaveBase,
+17
View File
@@ -155,6 +155,22 @@ export interface ElaprDocPrgrsClientResponseDto {
dcarbYn?: string;
}
/** 문서 상태 변경 이력 응답 DTO */
export interface DocSttsChgHstryResponseDto {
chgHstrySn: number;
docSeComCd: string;
docSeComNm: string;
docNo: string;
chgBfrSttsComCd: string;
chgBfrSttsComNm: string;
chgAftrSttsComCd: string;
chgAftrSttsComNm: string;
rmrk: string;
rgtrUuid: string;
rgtrNm: string;
regDt: string;
}
/** 사업규모 저장 공통 필드 */
export interface BzScaleSaveBase {
cnstpSeCd?: string;
@@ -244,4 +260,5 @@ export interface PbadmsMngDetResponseBase<TDet, TRvwOpnn = never, TSplmntDmnd =
rvwList?: TRvwOpnn[];
splmntDmndList?: TSplmntDmnd[];
elaprDocPrgrsList?: ElaprDocPrgrsClientResponseDto[] | null;
sttsChgHstryList?: DocSttsChgHstryResponseDto[];
}
+1
View File
@@ -1,4 +1,5 @@
export { NoData } from "./no-data";
export { PageLayout } from "./page-layout";
export { SectionCollapsible } from "./section-collapsible";
export type { TextShimmerProps } from "./text-shimmer";
export { TEXT_SHIMMER_CLASS, TextShimmer } from "./text-shimmer";
@@ -1,37 +1,37 @@
"use client";
import type { ReactNode } from "react";
import { ChevronDown } from "lucide-react";
import { cn, Collapsible, CollapsibleContent, CollapsibleTrigger } from "@common/components";
interface SectionCollapsibleProps {
title: ReactNode;
defaultOpen?: boolean;
children: ReactNode;
}
function SectionCollapsible({ title, defaultOpen = true, children }: SectionCollapsibleProps) {
return (
<Collapsible defaultOpen={defaultOpen} className="mx-4 rounded-sm border bg-background">
<CollapsibleTrigger asChild>
<button
type="button"
className="group flex w-full cursor-pointer items-center justify-between border-b p-4 text-left data-[state=closed]:border-b-0"
>
<h2 className="text-2xl font-bold">{title}</h2>
<ChevronDown
className={cn(
"size-6 shrink-0 text-muted-foreground transition-transform",
"group-data-[state=open]:rotate-180",
)}
/>
</button>
</CollapsibleTrigger>
<CollapsibleContent className="p-2">{children}</CollapsibleContent>
</Collapsible>
);
}
export { SectionCollapsible };
"use client";
import type { ReactNode } from "react";
import { ChevronDown } from "lucide-react";
import { cn, Collapsible, CollapsibleContent, CollapsibleTrigger } from "@common/components";
interface SectionCollapsibleProps {
title: ReactNode;
defaultOpen?: boolean;
children: ReactNode;
}
function SectionCollapsible({ title, defaultOpen = true, children }: SectionCollapsibleProps) {
return (
<Collapsible defaultOpen={defaultOpen} className="mx-4 rounded-sm border bg-background">
<CollapsibleTrigger asChild>
<button
type="button"
className="group flex w-full cursor-pointer items-center justify-between border-b p-4 text-left data-[state=closed]:border-b-0"
>
<h2 className="text-2xl font-bold">{title}</h2>
<ChevronDown
className={cn(
"size-6 shrink-0 text-muted-foreground transition-transform",
"group-data-[state=open]:rotate-180",
)}
/>
</button>
</CollapsibleTrigger>
<CollapsibleContent className="p-2">{children}</CollapsibleContent>
</Collapsible>
);
}
export { SectionCollapsible };
@@ -12,12 +12,12 @@ import {
import { useDataTable } from "@/shared/lib";
import type { PageResponse } from "@/shared/model";
import { SectionCollapsible } from "@/shared/ui";
import { NoData } from "@/shared/ui/no-data";
import type { CmCdAdstDstrComboResponseDto } from "@/entities/adstdstr";
import { useFindGugunCombo, useFindSidoCombo } from "@/entities/adstdstr";
import type { BnsDetailResponseDto, BnsListResponseDto, BnsSearchDto } from "@/entities/bns-if";
import { buildBnsSearchDto, useBnsDetail, useBnsList } from "@/entities/bns-if";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import {
Button,
Dialog,
@@ -1,4 +1,5 @@
import React from "react";
import { formatFileSize } from "@/shared/lib/file-utils";
type EtcFile = {
+1 -1
View File
@@ -39,7 +39,7 @@ export {
type PbadmsTaskApplySelection,
} from "./ui/pbadms-task-apply-dialog";
export { PrivacyConsentSection } from "./ui/privacy-consent-section";
export { SectionCollapsible } from "./ui/section-collapsible";
export { ProgressStatusSection } from "./ui/progress-status-section";
export {
SplmntDmndDialog,
type SplmntDmndDialogConfirmItem,
@@ -0,0 +1,59 @@
import type {
DocSttsChgHstryResponseDto,
ElaprDocPrgrsClientResponseDto,
} from "@/shared/model/pbadms-sprt";
interface ProgressStatusItem {
rgtrNm: string;
regDt: string;
}
const TECH_REVIEW_STTS_COM_CD = "TE";
const TECH_REVIEW_ATRZ_SE_COM_CD = "ATRZ02";
const TECH_REVIEW_ATRZ_PRGRS_STTS_COM_CD = "ATRZ_PRGRS_003";
function findProgressStatusItem(
sttsChgHstryList: DocSttsChgHstryResponseDto[],
sttsComCd: string,
): ProgressStatusItem[] {
return sttsChgHstryList
.filter((item) => item.chgAftrSttsComCd === sttsComCd)
.map((item) => ({
rgtrNm: item.rgtrNm,
regDt: item.regDt,
}));
}
function findTechReviewProgressItem(
elaprDocPrgrsList: ElaprDocPrgrsClientResponseDto[],
): ProgressStatusItem[] {
return elaprDocPrgrsList
.filter(
(item) =>
item.atrzSeComCd === TECH_REVIEW_ATRZ_SE_COM_CD &&
item.atrzPrgrsSttsComCd === TECH_REVIEW_ATRZ_PRGRS_STTS_COM_CD,
)
.map((item) => ({
rgtrNm: item.aprvrNm ?? "",
regDt: item.atrzDt ?? "",
}));
}
function resolveProgressStatusItem(
sttsComCd: string,
sttsChgHstryList: DocSttsChgHstryResponseDto[],
elaprDocPrgrsList: ElaprDocPrgrsClientResponseDto[],
): ProgressStatusItem[] {
if (sttsComCd === TECH_REVIEW_STTS_COM_CD) {
return findTechReviewProgressItem(elaprDocPrgrsList);
}
return findProgressStatusItem(sttsChgHstryList, sttsComCd);
}
export {
findProgressStatusItem,
findTechReviewProgressItem,
resolveProgressStatusItem,
TECH_REVIEW_STTS_COM_CD,
};
@@ -9,6 +9,7 @@ import { ENDPOINT } from "@/shared/config";
import { useTus } from "@/shared/lib";
import type { AtflMngClientResponseDto, EsntlAtchFileMngTreeResponseDto } from "@/shared/model";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { attachService } from "@/entities/attach";
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
import {
@@ -29,9 +30,7 @@ import {
type FieldPrefix,
isEtcFileKey,
toFileAccept,
} from "../lib/attachments-form-utils";
import { SectionCollapsible } from "./section-collapsible";
} from "../lib/attachments-form-utils";
interface AttachmentsFormData {
esntlAtchFileList: Array<{
@@ -3,6 +3,7 @@
import { useEffect, useMemo } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { SectionCollapsible } from "@/shared/ui";
import { Field } from "@/shared/ui/field";
import { useAcntgSeMngTree } from "@/entities/acntg";
import {
@@ -17,9 +18,7 @@ import {
} from "@common/components";
import { buildAcntgCostItems, recalculateBizCstTotals } from "../lib/business-cost-form-utils";
import { calcDiffPercent } from "../lib/format-utils";
import { SectionCollapsible } from "./section-collapsible";
import { calcDiffPercent } from "../lib/format-utils";
interface BusinessCostFormData {
bizCstList: Array<{
@@ -3,6 +3,7 @@
import { useEffect, useMemo } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { SectionCollapsible } from "@/shared/ui";
import { Field } from "@/shared/ui/field";
import { useCnstpSeMngTree } from "@/entities/cnstp";
import {
@@ -16,9 +17,7 @@ import {
TableRow,
} from "@common/components";
import { buildCnstpScaleItems, recalculateBzScaleTotals } from "../lib/business-scale-form-utils";
import { SectionCollapsible } from "./section-collapsible";
import { buildCnstpScaleItems, recalculateBzScaleTotals } from "../lib/business-scale-form-utils";
interface BusinessScaleFormData {
bzScaleList: Array<{
@@ -5,6 +5,7 @@ import { Controller, useFormContext, useWatch } from "react-hook-form";
import { useQuery } from "@tanstack/react-query";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldLabel } from "@/shared/ui/field";
import { cklstMngService, type CklstTreeNode } from "@/entities/cklst";
import { useCodeList } from "@/entities/code";
@@ -29,9 +30,7 @@ import {
buildChecklistTabs,
normalizeTaskSeComCdArray,
syncCklstResultListFromTree,
} from "../lib/checklist-form-utils";
import { SectionCollapsible } from "./section-collapsible";
} from "../lib/checklist-form-utils";
type FieldPrefix = "fnrsCnsltnMng" | "instlSncnMng" | "designCnstnMng" | "sewerBscPlan";
@@ -3,8 +3,8 @@
import { Controller, useFormContext } from "react-hook-form";
import { useUserStore } from "@/shared/lib";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldError } from "@/shared/ui/field";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import { Input, Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
type FieldPrefix =
@@ -5,6 +5,7 @@ import { Controller, useFormContext, useWatch } from "react-hook-form";
import { getDefaultFileKey, useTus } from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { Field } from "@/shared/ui/field";
import { useAcntgSeMngTree } from "@/entities/acntg";
import type { AtflMngClientResponseDto } from "@/entities/fnrs-cnsltn";
@@ -34,7 +35,6 @@ import {
import { calcDiffPercent } from "../lib/format-utils";
import { BizCstTotalDisplayTable } from "./biz-cst-total-display-table";
import { SectionCollapsible } from "./section-collapsible";
const SHAR_FILE_KEY_PREFIX = "fnrs-shar::";
@@ -2,6 +2,7 @@
import { Controller, useFormContext } from "react-hook-form";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldError, FieldLabel, FieldSet } from "@/shared/ui/field";
import {
RadioGroup,
@@ -11,9 +12,7 @@ import {
TableHead,
TableHeader,
TableRow,
} from "@common/components";
import { SectionCollapsible } from "./section-collapsible";
} from "@common/components";
interface PrivacyConsentFormData {
privacyConsent: "Y" | "N";
@@ -0,0 +1,88 @@
"use client";
import dayjs from "dayjs";
import { DocSttsChgHstryResponseDto } from "@/shared/model/pbadms-sprt";
import { SectionCollapsible } from "@/shared/ui";
import { useElaprDocPrgrsByDocNo } from "@/entities/elapr";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
import { resolveProgressStatusItem } from "../lib/progress-status-utils";
interface ProgressStatusColumn {
label: string;
sttsComCd: string;
}
interface ProgressStatusItem {
rgtrNm: string;
regDt: string;
}
interface ProgressStatusSectionProps {
columns: ProgressStatusColumn[];
sttsChgHstryList: DocSttsChgHstryResponseDto[];
elaprDocNo: string;
}
interface ProgressStatusCellProps {
items: ProgressStatusItem[];
}
function ProgressStatusCell({ items }: ProgressStatusCellProps) {
if (items.length === 0) return "-";
return (
<div className="flex flex-col gap-2">
{items.map((item, index) => (
<div key={`${item.rgtrNm}-${item.regDt}-${index}`}>
{item.rgtrNm || "-"}
{item.regDt && (
<p className="mt-1 text-lg text-muted-foreground">
{dayjs(item.regDt).format("YYYY-MM-DD")}
</p>
)}
</div>
))}
</div>
);
}
function ProgressStatusSection({
columns,
sttsChgHstryList,
elaprDocNo,
}: ProgressStatusSectionProps) {
const { data: elaprDocPrgrsList = [] } = useElaprDocPrgrsByDocNo(elaprDocNo);
return (
<SectionCollapsible title="진행 상태">
<Table variant="secondary">
<TableHeader>
<TableRow>
{columns.map((column) => (
<TableHead key={column.sttsComCd}>{column.label}</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
{columns.map((column) => (
<TableCell key={column.sttsComCd} align="center">
<ProgressStatusCell
items={resolveProgressStatusItem(
column.sttsComCd,
sttsChgHstryList,
elaprDocPrgrsList,
)}
/>
</TableCell>
))}
</TableRow>
</TableBody>
</Table>
</SectionCollapsible>
);
}
export { ProgressStatusSection };
@@ -7,6 +7,7 @@ import { Paperclip } from "lucide-react";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { attachService, useDownloadAttachmentByAtflCd } from "@/entities/attach";
import {
Badge,
@@ -20,8 +21,6 @@ import {
Textarea,
} from "@common/components";
import { SectionCollapsible } from "./section-collapsible";
const SPLMNT_CMPTN_FILE_KEY_PREFIX = "splmnt-cmptn::";
interface UploadedSplmntFile {
@@ -4,6 +4,7 @@ import { Paperclip } from "lucide-react";
import { dayjs } from "@/shared/lib";
import type { SplmntDmndResponseExtension, SplmntDmndSaveBase } from "@/shared/model";
import { SectionCollapsible } from "@/shared/ui";
import { useDownloadAttachmentByAtflCd } from "@/entities/attach";
import {
Badge,
@@ -15,8 +16,6 @@ import {
TableRow,
} from "@common/components";
import { SectionCollapsible } from "./section-collapsible";
type SplmntDmndItem = SplmntDmndSaveBase & SplmntDmndResponseExtension;
interface SupplementDemandSectionProps {
+6
View File
@@ -1,5 +1,11 @@
@tailwind components;
/* RealGrid 기본은 white-space: nowrap → max-width만 주면 잘림. 너비 초과 시에만 줄바꿈 */
.rg-tooltip {
max-width: 60rem;
white-space: pre-line;
}
@layer components {
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell {
@apply !bg-white;
+196 -54
View File
@@ -86,6 +86,40 @@ function mergeGridRowsWithSourceMetadata(
});
}
/** row.verificationResult 필드별 오류 배열이 하나라도 있으면 true */
function rowHasVerificationError(
row: Record<string, string | number>,
): boolean {
const verification = (
row as { verificationResult?: Record<string, unknown> }
).verificationResult;
if (!verification || typeof verification !== "object") return false;
for (const value of Object.values(verification)) {
if (Array.isArray(value) && value.length > 0) return true;
}
return false;
}
function rowsHaveVerificationErrors(
rows: Record<string, string | number>[],
): boolean {
return rows.some(rowHasVerificationError);
}
function sortRowsVerifyErrorsFirst(
rows: Record<string, string | number>[],
): Record<string, string | number>[] {
return rows
.map((row, index) => ({ row, index }))
.sort((a, b) => {
const aErr = rowHasVerificationError(a.row) ? 0 : 1;
const bErr = rowHasVerificationError(b.row) ? 0 : 1;
if (aErr !== bErr) return aErr - bErr;
return a.index - b.index;
})
.map(({ row }) => row);
}
function applyCellStyleCallbacksToColumns(
gridView: unknown,
columns: ColumnConfig[],
@@ -804,6 +838,12 @@ const DataGrid = ({
const emptyCellHoverRafRef = useRef<number | null>(null);
const [emptyCellHoverTooltip, setEmptyCellHoverTooltip] =
useState<EmptyCellHoverTooltip | null>(null);
/** 검증 오류 행 우선보기 (오류 있는 데이터일 때만 UI 노출) */
const [preferVerifyErrors, setPreferVerifyErrors] = useState(false);
const verifyPriorityBaselineRef = useRef<
Record<string, string | number>[] | null
>(null);
const skipVerifyPriorityResetRef = useRef(false);
columnsRef.current = columns;
rowDataRef.current = rowData;
cellStyleCallbackRef.current = cellStyleCallback;
@@ -814,6 +854,90 @@ const DataGrid = ({
onCellEditedRef.current = onCellEdited;
onDataCellClickRef.current = onDataCellClick;
const hasVerificationErrors = rowsHaveVerificationErrors(rowData);
const showVerifyPriorityToggle = hasVerificationErrors;
// 조회 등으로 rowData가 교체되면 우선보기 해제 (자체 setRowData는 스킵)
useEffect(() => {
if (skipVerifyPriorityResetRef.current) {
skipVerifyPriorityResetRef.current = false;
return;
}
verifyPriorityBaselineRef.current = null;
setPreferVerifyErrors(false);
}, [rowData]);
// 오류가 없어지면 체크 해제 + 원본 순서 복원
useEffect(() => {
if (hasVerificationErrors || !preferVerifyErrors) return;
const baseline = verifyPriorityBaselineRef.current;
verifyPriorityBaselineRef.current = null;
setPreferVerifyErrors(false);
if (baseline) {
skipVerifyPriorityResetRef.current = true;
setRowData(baseline);
}
}, [hasVerificationErrors, preferVerifyErrors, setRowData]);
const collectCurrentRowsWithMetadata = () => {
const fieldNames = columns
.map((c) => c.fieldName)
.filter(Boolean) as string[];
const gv = gridRef.current?.gridView as
| {
getDataSource?: () => {
getRowCount(): number;
getValue(r: number, f: string): unknown;
};
commit?: (force?: boolean) => boolean;
}
| undefined;
if (!gv?.getDataSource) {
return rowDataRef.current.map((r) => ({ ...r }));
}
try {
gv.commit?.();
} catch {
/* ignore */
}
const ds = gv.getDataSource();
if (!ds) return rowDataRef.current.map((r) => ({ ...r }));
const cnt = ds.getRowCount();
const rows: Record<string, string | number>[] = [];
for (let i = 0; i < cnt; i++) {
const r: Record<string, string | number> = {};
for (const f of fieldNames)
r[f] = ds.getValue(i, f) as string | number;
rows.push(r);
}
return mergeGridRowsWithSourceMetadata(
rows,
rowDataRef.current,
fieldNames,
);
};
const handlePreferVerifyErrorsChange = (checked: boolean) => {
if (checked) {
const current = collectCurrentRowsWithMetadata();
if (!rowsHaveVerificationErrors(current)) return;
verifyPriorityBaselineRef.current = current.map((r) => ({ ...r }));
sortDirRef.current = {};
originalRowsRef.current = {};
skipVerifyPriorityResetRef.current = true;
setPreferVerifyErrors(true);
setRowData(sortRowsVerifyErrorsFirst(current));
return;
}
const baseline = verifyPriorityBaselineRef.current;
verifyPriorityBaselineRef.current = null;
skipVerifyPriorityResetRef.current = true;
setPreferVerifyErrors(false);
if (baseline) {
setRowData(baseline);
}
};
// rowData 변경 시 편집 중인 셀을 commit한 뒤 수동으로 setRows (RealGridReact에 rows prop을 넘기지 않음)
const rowDataMountedRef = useRef(false);
useEffect(() => {
@@ -2086,60 +2210,79 @@ useEffect(() => {
onChange={handleFileChange}
/>
<div
className={`flex items-center ${statusInfo ? "justify-between" : "justify-end"} gap-[0.6rem] pr-[1.2rem]`}
className={`flex items-center ${statusInfo || showVerifyPriorityToggle ? "justify-between" : "justify-end"} gap-[0.6rem] pr-[1.2rem]`}
>
{statusInfo && (
<div className="data-grid-status-bubble flex flex-wrap items-center gap-x-4 gap-y-1">
<span>
진행상황:{" "}
{statusInfo.laeSbmsnSttsCd === null ||
statusInfo.laeSbmsnSttsCd === "0" ? (
"미작성"
) : statusInfo.laeSbmsnSttsCd === "1" ? (
<span className="animate-pulse">작성중</span>
) : statusInfo.laeSbmsnSttsCd === "2" ? (
<span className="!text-[#2094fa]">제출</span>
) : statusInfo.laeSbmsnSttsCd === "3" ? (
<span className="!text-[#1fd078]">승인</span>
) : statusInfo.laeSbmsnSttsCd === "4" ? (
<span className="relative inline-flex pr-3 !text-[#ff6c64]">
반려
<span className="absolute -right-0.5 -top-0.5 inline-flex size-[0.6125rem]">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[#ff6c64] opacity-75" />
<span className="relative inline-flex size-[0.6125rem] rounded-full bg-[#ff6c64]" />
<div className="flex min-w-0 flex-wrap items-center gap-3">
{statusInfo && (
<div className="data-grid-status-bubble flex flex-wrap items-center gap-x-4 gap-y-1">
<span>
진행상황:{" "}
{statusInfo.laeSbmsnSttsCd === null ||
statusInfo.laeSbmsnSttsCd === "0" ? (
"미작성"
) : statusInfo.laeSbmsnSttsCd === "1" ? (
<span className="animate-pulse">작성중</span>
) : statusInfo.laeSbmsnSttsCd === "2" ? (
<span className="!text-[#2094fa]">제출</span>
) : statusInfo.laeSbmsnSttsCd === "3" ? (
<span className="!text-[#1fd078]">승인</span>
) : statusInfo.laeSbmsnSttsCd === "4" ? (
<span className="relative inline-flex pr-3 !text-[#ff6c64]">
반려
<span className="absolute -right-0.5 -top-0.5 inline-flex size-[0.6125rem]">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[#ff6c64] opacity-75" />
<span className="relative inline-flex size-[0.6125rem] rounded-full bg-[#ff6c64]" />
</span>
</span>
</span>
) : (
<span className="animate-pulse">검증중</span>
)}
</span>
<span>
반려사유:{" "}
{statusInfo.rjctOpnnCn
? statusInfo.rjctOpnnCn
: "-"}
</span>
<span>
제출일:{" "}
{statusInfo.laeSbmsnYmd
? statusInfo.laeSbmsnYmd.length === 8 &&
/^\d{8}$/.test(statusInfo.laeSbmsnYmd)
? `${statusInfo.laeSbmsnYmd.slice(0, 4)}-${statusInfo.laeSbmsnYmd.slice(4, 6)}-${statusInfo.laeSbmsnYmd.slice(6, 8)}`
: statusInfo.laeSbmsnYmd
: "미제출"}
</span>
<span>
담당자:{" "}
{statusInfo.laePicNm ? statusInfo.laePicNm : "-"}
</span>
<span>
연락처:{" "}
{statusInfo.laePicTelNo
? statusInfo.laePicTelNo
: "-"}
</span>
</div>
)}
) : (
<span className="animate-pulse">검증중</span>
)}
</span>
<span>
반려사유:{" "}
{statusInfo.rjctOpnnCn
? statusInfo.rjctOpnnCn
: "-"}
</span>
<span>
제출일:{" "}
{statusInfo.laeSbmsnYmd
? statusInfo.laeSbmsnYmd.length === 8 &&
/^\d{8}$/.test(statusInfo.laeSbmsnYmd)
? `${statusInfo.laeSbmsnYmd.slice(0, 4)}-${statusInfo.laeSbmsnYmd.slice(4, 6)}-${statusInfo.laeSbmsnYmd.slice(6, 8)}`
: statusInfo.laeSbmsnYmd
: "미제출"}
</span>
<span>
담당자:{" "}
{statusInfo.laePicNm ? statusInfo.laePicNm : "-"}
</span>
<span>
연락처:{" "}
{statusInfo.laePicTelNo
? statusInfo.laePicTelNo
: "-"}
</span>
</div>
)}
{showVerifyPriorityToggle && (
<label className="inline-flex shrink-0 cursor-pointer items-center gap-1.5 text-label data-grid-status-bubble">
<input
type="checkbox"
className="m-0 shrink-0 cursor-pointer accent-primary"
checked={preferVerifyErrors}
onChange={(e) =>
handlePreferVerifyErrorsChange(
e.target.checked,
)
}
/>
<span className="whitespace-nowrap leading-none">
검증 오류 항목 우선보기
</span>
</label>
)}
</div>
<div className="flex items-center gap-[0.6rem]">
{isExcelImport && (
<Button
@@ -2467,11 +2610,10 @@ useEffect(() => {
{emptyCellHoverTooltip &&
createPortal(
<div
className="rg-tooltip pointer-events-none !visible fixed z-[9999]"
className="rg-tooltip pointer-events-none !visible fixed z-[9999] w-max=[60rem]"
style={{
left: emptyCellHoverTooltip.x,
top: emptyCellHoverTooltip.y,
whiteSpace: "pre-wrap",
}}
role="tooltip"
>
@@ -22,6 +22,7 @@ import {
LoadingWithAnimation,
} from "@common/components";
import DataGrid from "@/components/module/DataGrid";
import { cellContentTooltip } from "@/components/module/verificationResultCellTooltip";
import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination";
import type {
CellEditedCallbackParams,
@@ -413,6 +414,7 @@ export function VerificationResultDialog({
getGridDataRef={getGridDataRef}
onCellEdited={onPrcsOpnnExplnEdited}
cellStyleCallback={verificationMthdCellStyle}
cellTooltipCallback={cellContentTooltip}
onItemChecked={(itemIndex, checked) => {
setCheckedIndices((prev) => {
const next = new Set(prev);
@@ -31,6 +31,7 @@ import {
} from "@common/components";
import DataGrid from "@/components/module/DataGrid";
import { cellContentTooltip } from "@/components/module/verificationResultCellTooltip";
import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination";
import type { CellStyleCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type";
import { useAlertStore } from "@/shared/lib";
@@ -500,6 +501,7 @@ export function VerificationResultHistoryDialog({
enableSort={false}
getGridRef={gridRef}
cellStyleCallback={verificationPrcsYnNmCellStyle}
cellTooltipCallback={cellContentTooltip}
onItemChecked={(itemIndex, checked) => {
const entry = entries[itemIndex];
if (!entry) return;
@@ -0,0 +1,11 @@
import type { CellTooltipCallbackParams } from "@/components/type/DataGrid.type";
/** RealGrid 셀 호버 시 title처럼 전체 내용 표시 */
export function cellContentTooltip({
row,
fieldName,
}: CellTooltipCallbackParams): string | undefined {
const value = row[fieldName];
if (value == null || value === "") return undefined;
return String(value);
}
@@ -52,8 +52,8 @@ const TYPE_CODE_COLOR_MAP: Record<string, BadgeProps["color"]> = {
const TYPE_CODE_NAME_MAP: Record<string, string> = {
NV: "일반",
IMR: "I-MR",
HB: "HB",
IMR: "시계열",
HB: "모집단",
};
const PAGE_SIZE = 100;
@@ -62,6 +62,7 @@ import type {
} from "@/entities/statistics/model/statistics-interface";
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import { useTabStore } from "@/store/tab-store";
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { useAdminStore } from "@/store/admin-store";
@@ -356,7 +357,10 @@ const ExcretaTreatmentFacilityInp = () => {
const sggOptionsSidoRef = useRef<string | null>(null);
const [searchLoaded, setSearchLoaded] = useState(false);
const activeTabId = useTabStore((s) => s.activeTabId);
useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.EXCRETA_TREATMENT_FACILITY) return;
let cancelled = false;
void (async () => {
try {
@@ -382,7 +386,7 @@ const ExcretaTreatmentFacilityInp = () => {
return () => {
cancelled = true;
};
}, []);
}, [activeTabId]);
const loadSggOptions = useCallback(async (sido: string): Promise<Sgg[]> => {
if (!sido || sido === "ALL") {
@@ -56,6 +56,8 @@ import { PrivateSewageDesigStatusInpColumn } from "@/components/util/column/inpu
import { SepticTankStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/SepticTankStatusInpColumn";
import { SewageTreatFactStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/SewageTreatFactStatusInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { useTabStore } from "@/store/tab-store";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
import { useAdminStore } from "@/store/admin-store";
@@ -74,10 +76,6 @@ const PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY: Record<string, s
[PrivateSewageDesigStatusInpColumn.tabKey]: "ST00080003",
};
const PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CDS = Array.from(
new Set(Object.values(PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY)),
);
type GridRowWithVerification = Record<string, string | number> & {
verificationResult?: PrivateSewageFacilityVerificationResult;
};
@@ -362,42 +360,30 @@ const PrivateSewageFacilityInp = () => {
const getGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
activeTabRef.current = activeTab;
const activeTabId = useTabStore((s) => s.activeTabId);
useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.PRIVATE_SEWAGE_FACILITY) return;
const tabKey = activeTab;
if (!tabKey) return;
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
const statsCd =
PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY[tabKey] ?? tabKey;
let cancelled = false;
void (async () => {
const entries = await Promise.all(
PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CDS.map(async (statsCd) => {
try {
const res = await statisticsApi.getColumnGuides(statsCd);
if (res.status !== "success" || !Array.isArray(res.data)) {
return [statsCd, {}] as const;
}
const next: Record<string, string> = {};
for (const item of res.data) {
const key = item.engColNm?.trim();
const guide = item.columnGuide?.trim();
if (key && guide) next[key] = guide;
}
return [statsCd, next] as const;
} catch {
return [statsCd, {}] as const;
}
}),
);
const next = await fetchColumnGuidesRecord(statsCd);
if (cancelled) return;
const guidesByStatsCd = Object.fromEntries(entries);
setColumnGuidesByTab(
Object.fromEntries(
Object.entries(PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY).map(
([tabKey, statsCd]) => [tabKey, guidesByStatsCd[statsCd] ?? {}],
),
),
setColumnGuidesByTab((prev) =>
Object.prototype.hasOwnProperty.call(prev, tabKey)
? prev
: { ...prev, [tabKey]: next },
);
})();
return () => {
cancelled = true;
};
}, []);
}, [activeTabId, activeTab, columnGuidesByTab]);
const commitToolbarSnapshotForLoadedTab = (
tabKey: string,
@@ -49,6 +49,8 @@ import type { SearchFormValues } from '@/components/type/SearchForm.type';
import { doList } from '@/components/util/column/input/DoList';
import { PublicSewageFactInpColumnList } from '@/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactInpColumn';
import { toGridRows } from '@/components/util/grid/gridFunction';
import { fetchColumnGuidesRecord } from '@/components/util/grid/columnGuides';
import { useTabStore } from '@/store/tab-store';
import { patchVerificationFailedByTab } from '@/components/util/input/verification-failed-flag';
import { useAdminStore } from '@/store/admin-store';
@@ -144,10 +146,6 @@ type MainFacilityMappingSaveDraft = {
initialPrxyCtrtCn: string;
};
const PUBLIC_SEWAGE_FACILITY_COLUMN_GUIDE_TAB_KEYS = Array.from(
new Set(PublicSewageFactInpColumnList.map((tab) => tab.tabKey).filter(Boolean)),
);
/** 위탁계약기간: `yyyymmdd~yyyymmdd` (빈 값 허용). 하이픈 형식은 정규화 */
function normalizePrxyCtrtCn(raw: unknown): string | null {
if (raw === null || raw === undefined) return null;
@@ -644,35 +642,28 @@ const PublicSewageFacilityInp = () => {
const firstTabKey = PublicSewageFactInpColumnList[0]?.tabKey ?? 'ST00060001';
const searchParamRef = useRef<PublicSewageFacilityRequestParams | null>(null);
const activeTabId = useTabStore((s) => s.activeTabId);
useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.PUBLIC_SEWAGE_FACILITY) return;
const tabKey = activeTab;
if (!tabKey) return;
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
let cancelled = false;
void (async () => {
const entries = await Promise.all(
PUBLIC_SEWAGE_FACILITY_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => {
try {
const res = await statisticsApi.getColumnGuides(tabKey);
if (res.status !== 'success' || !Array.isArray(res.data)) {
return [tabKey, {}] as const;
}
const next: Record<string, string> = {};
for (const item of res.data) {
const key = item.engColNm?.trim();
const guide = item.columnGuide?.trim();
if (key && guide) next[key] = guide;
}
return [tabKey, next] as const;
} catch {
return [tabKey, {}] as const;
}
}),
);
const next = await fetchColumnGuidesRecord(tabKey);
if (cancelled) return;
setColumnGuidesByTab(Object.fromEntries(entries));
setColumnGuidesByTab((prev) =>
Object.prototype.hasOwnProperty.call(prev, tabKey)
? prev
: { ...prev, [tabKey]: next },
);
})();
return () => {
cancelled = true;
};
}, []);
}, [activeTabId, activeTab, columnGuidesByTab]);
const commitToolbarSnapshotForLoadedTab = (
tabKey: string,
@@ -61,6 +61,7 @@ import type {
} from "@/entities/statistics/model/statistics-interface";
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import { useTabStore } from "@/store/tab-store";
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { useAdminStore } from "@/store/admin-store";
@@ -353,7 +354,10 @@ const PumpingStationInp = () => {
const sggOptionsSidoRef = useRef<string | null>(null);
const [searchLoaded, setSearchLoaded] = useState(false);
const activeTabId = useTabStore((s) => s.activeTabId);
useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.PUMPING_STATION) return;
let cancelled = false;
void (async () => {
try {
@@ -377,7 +381,7 @@ const PumpingStationInp = () => {
return () => {
cancelled = true;
};
}, []);
}, [activeTabId]);
const loadSggOptions = useCallback(async (sido: string): Promise<Sgg[]> => {
if (!sido || sido === "ALL") {
@@ -36,6 +36,8 @@ import { PrivateSewageTreatRelatedIndustryInpColumn } from "@/components/util/co
import { PublicInstitutionExcretaIndustryInpColumn } from "@/components/util/column/input/sewageExcretaRelated/PublicInstitutionExcretaIndustryInpColumn";
import { PublicInstitutionSewerIndustryInpColumn } from "@/components/util/column/input/sewageExcretaRelated/PublicInstitutionSewerIndustryInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { useTabStore } from "@/store/tab-store";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
import { statisticsInputSewageExcretaRelatedApi } from "@/entities/statistics/api/input/statistics-input-sewage-excreta-related-service";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
@@ -645,36 +647,29 @@ const SewageExcretaRelatedInp = () => {
const sggOptionsSidoRef = useRef<string | null>(null);
activeTabRef.current = activeTab;
const activeTabId = useTabStore((s) => s.activeTabId);
useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_EXCRETA_RELATED) return;
const tabKey = activeTab;
if (!tabKey) return;
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
const statsCd = tabKey;
let cancelled = false;
void (async () => {
const entries = await Promise.all(
SewageExcretaRelatedInpTabList.map(async (tab) => {
const tabKey = tab.tabKey;
try {
const res = await statisticsApi.getColumnGuides(tabKey);
if (res.status !== "success" || !Array.isArray(res.data)) {
return [tabKey, {}] as const;
}
const next: Record<string, string> = {};
for (const item of res.data) {
const key = item.engColNm?.trim();
const guide = item.columnGuide?.trim();
if (key && guide) next[key] = guide;
}
return [tabKey, next] as const;
} catch {
return [tabKey, {}] as const;
}
}),
);
const next = await fetchColumnGuidesRecord(statsCd);
if (cancelled) return;
setColumnGuidesByTab(Object.fromEntries(entries));
setColumnGuidesByTab((prev) =>
Object.prototype.hasOwnProperty.call(prev, tabKey)
? prev
: { ...prev, [tabKey]: next },
);
})();
return () => {
cancelled = true;
};
}, []);
}, [activeTabId, activeTab, columnGuidesByTab]);
const shouldShowNotApplicable = (
tabKey: string,
@@ -50,6 +50,8 @@ import { SewageSlurgeProcessingFacStatusInpColumn } from "@/components/util/colu
import { SlurgeProcessingFacStatusInpColumn } from "@/components/util/column/input/sewageExcretaSlurge/SlurgeProcessingFacStatusInpColumn";
import { doList } from "@/components/util/column/input/DoList";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { useTabStore } from "@/store/tab-store";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
import { statisticsInputSewageExcretaSlurgeApi } from "@/entities/statistics/api/input/statistics-input-sewage-excreta-slurge-service";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
@@ -700,36 +702,29 @@ const SewageExcretaSlurgeInp = () => {
const preYearArticleFcltSnRef = useRef<ArticleFcltSnSet>(new Set());
activeTabRef.current = activeTab;
const activeTabId = useTabStore((s) => s.activeTabId);
useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_EXCRETA_SLURGE) return;
const tabKey = activeTab;
if (!tabKey) return;
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
const statsCd = tabKey;
let cancelled = false;
void (async () => {
const entries = await Promise.all(
SewageExcretaSlurgeInpTabList.map(async (tab) => {
const tabKey = tab.tabKey;
try {
const res = await statisticsApi.getColumnGuides(tabKey);
if (res.status !== "success" || !Array.isArray(res.data)) {
return [tabKey, {}] as const;
}
const next: Record<string, string> = {};
for (const item of res.data) {
const key = item.engColNm?.trim();
const guide = item.columnGuide?.trim();
if (key && guide) next[key] = guide;
}
return [tabKey, next] as const;
} catch {
return [tabKey, {}] as const;
}
}),
);
const next = await fetchColumnGuidesRecord(statsCd);
if (cancelled) return;
setColumnGuidesByTab(Object.fromEntries(entries));
setColumnGuidesByTab((prev) =>
Object.prototype.hasOwnProperty.call(prev, tabKey)
? prev
: { ...prev, [tabKey]: next },
);
})();
return () => {
cancelled = true;
};
}, []);
}, [activeTabId, activeTab, columnGuidesByTab]);
const shouldShowNotApplicable = (
hasRows: boolean,
@@ -70,6 +70,7 @@ import { PipeTypeStatusInpColumn } from "@/components/util/column/input/sewagePi
import { RegionalFactStatusInpColumn } from "@/components/util/column/input/sewagePipe/RegionalFactStatusInpColumn";
import { SewagePipeInpColumnInfoList } from "@/components/util/column/input/sewagePipe/SewagePipeInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import {
patchVerificationFailedByTab,
} from "@/components/util/input/verification-failed-flag";
@@ -297,9 +298,6 @@ type GridRowWithVerification = Record<string, string | number> & {
};
const REGIONAL_FACT_TAB_KEY = "ST00020001";
const SEWAGE_PIPE_COLUMN_GUIDE_TAB_KEYS = Array.from(
new Set(SewagePipeInpColumnInfoList.map((tab) => tab.tabKey).filter(Boolean)),
);
// function hasRegionalFactVerificationErrors(verification?: SeweragePipeInputVerificationResult | null): boolean {
// if (!verification) return false;
@@ -538,34 +536,24 @@ const SewagePipeInp = () => {
useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE) return;
const tabKey = activeTab;
if (!tabKey) return;
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
let cancelled = false;
void (async () => {
const entries = await Promise.all(
SEWAGE_PIPE_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => {
try {
const res = await statisticsApi.getColumnGuides(tabKey);
if (res.status !== "success" || !Array.isArray(res.data)) {
return [tabKey, {}] as const;
}
const next: Record<string, string> = {};
for (const item of res.data) {
const key = item.engColNm?.trim();
const guide = item.columnGuide?.trim();
if (key && guide) next[key] = guide;
}
return [tabKey, next] as const;
} catch {
return [tabKey, {}] as const;
}
}),
);
const next = await fetchColumnGuidesRecord(tabKey);
if (cancelled) return;
setColumnGuidesByTab(Object.fromEntries(entries));
setColumnGuidesByTab((prev) =>
Object.prototype.hasOwnProperty.call(prev, tabKey)
? prev
: { ...prev, [tabKey]: next },
);
})();
return () => {
cancelled = true;
};
}, [activeTabId]);
}, [activeTabId, activeTab, columnGuidesByTab]);
const syncTopStatusIfStillOnTab = (loadedTabKey: string, nextStatsCd: string) => {
if (loadedTabKey === activeTabRef.current) {
@@ -57,6 +57,7 @@ import { doList } from "@/components/util/column/input/DoList";
import { PipeDredgingStatusInpColumn } from "@/components/util/column/input/sewagePipeMaintain/PipeDredgingStatusInpColumn";
import { RepairStatusInpColumn } from "@/components/util/column/input/sewagePipeMaintain/RepairStatusInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
import { useAdminStore } from "@/store/admin-store";
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
@@ -82,12 +83,6 @@ const SewagePipeMaintainInpTabList = [
const firstTabKey = RepairStatusInpColumn.tabKey;
const SEWAGE_PIPE_MAINTAIN_COLUMN_GUIDE_TAB_KEYS = Array.from(
new Set(
SewagePipeMaintainInpTabList.map((tab) => tab.tabKey).filter(Boolean),
),
);
const toGridColumnInfo = (tab: (typeof SewagePipeMaintainInpTabList)[number]): ColumnInfo => ({
columns: tab.columns ?? [],
columnLayout: tab.columnLayout ?? [],
@@ -344,34 +339,25 @@ const SewagePipeMaintainInp = () => {
useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE_MAINTAIN) return;
const tabKey = activeTab;
if (!tabKey) return;
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
const statsCd = tabKey;
let cancelled = false;
void (async () => {
const entries = await Promise.all(
SEWAGE_PIPE_MAINTAIN_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => {
try {
const res = await statisticsApi.getColumnGuides(tabKey);
if (res.status !== "success" || !Array.isArray(res.data)) {
return [tabKey, {}] as const;
}
const next: Record<string, string> = {};
for (const item of res.data) {
const key = item.engColNm?.trim();
const guide = item.columnGuide?.trim();
if (key && guide) next[key] = guide;
}
return [tabKey, next] as const;
} catch {
return [tabKey, {}] as const;
}
}),
);
const next = await fetchColumnGuidesRecord(statsCd);
if (cancelled) return;
setColumnGuidesByTab(Object.fromEntries(entries));
setColumnGuidesByTab((prev) =>
Object.prototype.hasOwnProperty.call(prev, tabKey)
? prev
: { ...prev, [tabKey]: next },
);
})();
return () => {
cancelled = true;
};
}, [activeTabId]);
}, [activeTabId, activeTab, columnGuidesByTab]);
const commitToolbarSnapshotForLoadedTab = (
tabKey: string,
@@ -43,6 +43,8 @@ import { doList } from "@/components/util/column/input/DoList";
import { RainfallPumpStationInpColumn } from "@/components/util/column/input/sewageReservoirPump/RainfallPumpStationInpColumn";
import { SewageReservoirInpColumn } from "@/components/util/column/input/sewageReservoirPump/SewageReservoirInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { useTabStore } from "@/store/tab-store";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
import {
formatCompactYmdForGrid,
@@ -92,12 +94,6 @@ const firstTabKey = SewageReservoirPumpInpTabList[0]?.tabKey ?? SEWAGE_RESERVOIR
const rainfallPumpTabKey =
RainfallPumpStationInpColumn.tabKey ?? "rainfallPumpStationInp";
const SEWAGE_RESERVOIR_PUMP_COLUMN_GUIDE_TAB_KEYS = Array.from(
new Set(
SewageReservoirPumpInpTabList.map((tab) => tab.tabKey).filter(Boolean),
),
);
const SEWAGE_RESERVOIR_FACILITY_NAME_GUIDE_MESSAGE =
"하수저류시설 등록시 하수저류시설명이 동일할 경우 같은 데이터로 분류됩니다.\n\n" +
"하수저류시설명이 동일 할 경우 구분을 위해 하수저류시설 1, 하수저류시설 2 형태로 구분하여 주시길 바랍니다.";
@@ -957,35 +953,29 @@ const SewageReservoirPumpInp = () => {
useRef<(() => Record<string, string | number>[]) | null>(null);
const sggOptionsSidoRef = useRef<string | null>(null);
const activeTabId = useTabStore((s) => s.activeTabId);
useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_RESERVOIR_PUMP) return;
const tabKey = activeTab;
if (!tabKey) return;
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
const statsCd = tabKey;
let cancelled = false;
void (async () => {
const entries = await Promise.all(
SEWAGE_RESERVOIR_PUMP_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => {
try {
const res = await statisticsApi.getColumnGuides(tabKey);
if (res.status !== "success" || !Array.isArray(res.data)) {
return [tabKey, {}] as const;
}
const next: Record<string, string> = {};
for (const item of res.data) {
const key = item.engColNm?.trim();
const guide = item.columnGuide?.trim();
if (key && guide) next[key] = guide;
}
return [tabKey, next] as const;
} catch {
return [tabKey, {}] as const;
}
}),
);
const next = await fetchColumnGuidesRecord(statsCd);
if (cancelled) return;
setColumnGuidesByTab(Object.fromEntries(entries));
setColumnGuidesByTab((prev) =>
Object.prototype.hasOwnProperty.call(prev, tabKey)
? prev
: { ...prev, [tabKey]: next },
);
})();
return () => {
cancelled = true;
};
}, []);
}, [activeTabId, activeTab, columnGuidesByTab]);
const searchDoneRef = useRef(false);
const periodEnterableRef = useRef(true);
@@ -61,6 +61,8 @@ import { SewerageChargeInpColumn } from "@/components/util/column/input/sewerage
import { SewerageFinanceSettleInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageFinanceSettleInpColumn";
import { SewerageUsageFeeInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { useTabStore } from "@/store/tab-store";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
import { useAdminStore } from "@/store/admin-store";
@@ -73,10 +75,6 @@ const SewerageFinanceInpTabList = [
const firstTabKey = SewerageFinanceSettleInpColumn.tabKey;
const SEWERAGE_FINANCE_COLUMN_GUIDE_TAB_KEYS = Array.from(
new Set(SewerageFinanceInpTabList.map((tab) => tab.tabKey).filter(Boolean)),
);
type GridRowWithVerification = Record<string, string | number> & {
verificationResult?: SewerageFinanceVerificationResult;
};
@@ -391,35 +389,29 @@ const SewerageFinanceInp = () => {
const getGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
activeTabRef.current = activeTab;
const activeTabId = useTabStore((s) => s.activeTabId);
useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWERAGE_FINANCE) return;
const tabKey = activeTab;
if (!tabKey) return;
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
const statsCd = tabKey;
let cancelled = false;
void (async () => {
const entries = await Promise.all(
SEWERAGE_FINANCE_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => {
try {
const res = await statisticsApi.getColumnGuides(tabKey);
if (res.status !== "success" || !Array.isArray(res.data)) {
return [tabKey, {}] as const;
}
const next: Record<string, string> = {};
for (const item of res.data) {
const key = item.engColNm?.trim();
const guide = item.columnGuide?.trim();
if (key && guide) next[key] = guide;
}
return [tabKey, next] as const;
} catch {
return [tabKey, {}] as const;
}
}),
);
const next = await fetchColumnGuidesRecord(statsCd);
if (cancelled) return;
setColumnGuidesByTab(Object.fromEntries(entries));
setColumnGuidesByTab((prev) =>
Object.prototype.hasOwnProperty.call(prev, tabKey)
? prev
: { ...prev, [tabKey]: next },
);
})();
return () => {
cancelled = true;
};
}, []);
}, [activeTabId, activeTab, columnGuidesByTab]);
const commitToolbarSnapshotForLoadedTab = (
tabKey: string,
@@ -490,7 +490,7 @@ const SeweragePenetrationRateInp = () => {
statsYr: searchValues?.year ?? '',
userId: userInfo.userUnqId,
submitList: convertedData,
sewerBzmnFrgnrWholPpltnCnt: footerFromApi?.sewerBzmnFrgnrWholPpltnCnt,
sewerBzmnArea: footerFromApi?.sewerBzmnArea,
laePicNm: submitInfo.laePicNm,
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
@@ -680,7 +680,7 @@ const SeweragePenetrationRateInp = () => {
statsYr: searchValues?.year ?? '', // 통계 연도
userId: userInfo.userUnqId, // 사용자
saveList: convertedData, // 저장 데이터
sewerBzmnFrgnrWholPpltnCnt: footerFromApi?.sewerBzmnFrgnrWholPpltnCnt // 행정참고자료 외국인 합계
sewerBzmnArea: footerFromApi?.sewerBzmnArea, // 행정참고자료면적
};
if (isAdminInfo) {
@@ -24,6 +24,8 @@ import { doList } from "@/components/util/column/input/DoList";
import { ComplaintInpColumn } from "@/components/util/column/input/sewerageServiceLevel/ComplaintInpColumn";
import { ResidentFriendlyFactInpColumn } from "@/components/util/column/input/sewerageServiceLevel/ResidentFriendlyFactInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { useTabStore } from "@/store/tab-store";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
import { statisticsInputSewerageServiceLevelApi } from "@/entities/statistics/api/input/statistics-input-sewerage-service-level-service";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
@@ -60,11 +62,6 @@ const SewerageServiceLevelInpTabList = [
const firstTabKey = ComplaintInpColumn.tabKey;
const SEWERAGE_SERVICE_LEVEL_COLUMN_GUIDE_STATS_CDS = [
SEWERAGE_SERVICE_LEVEL_COMPLAINT_STATS_CD,
SEWERAGE_SERVICE_LEVEL_RESIDENT_FRIENDLY_FACT_STATS_CD,
] as const;
const SEWERAGE_SERVICE_LEVEL_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY: Record<string, string> = {
[ComplaintInpColumn.tabKey]: SEWERAGE_SERVICE_LEVEL_COMPLAINT_STATS_CD,
[ResidentFriendlyFactInpColumn.tabKey]: SEWERAGE_SERVICE_LEVEL_RESIDENT_FRIENDLY_FACT_STATS_CD,
@@ -326,42 +323,30 @@ const SewerageServiceLevelInp = () => {
const publicSewageFacilitiesAvailableRef = useRef(false);
activeTabRef.current = activeTab;
const activeTabId = useTabStore((s) => s.activeTabId);
useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWERAGE_SERVICE_LEVEL) return;
const tabKey = activeTab;
if (!tabKey) return;
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
const statsCd =
SEWERAGE_SERVICE_LEVEL_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY[tabKey] ?? tabKey;
let cancelled = false;
void (async () => {
const entries = await Promise.all(
SEWERAGE_SERVICE_LEVEL_COLUMN_GUIDE_STATS_CDS.map(async (statsCd) => {
try {
const res = await statisticsApi.getColumnGuides(statsCd);
if (res.status !== "success" || !Array.isArray(res.data)) {
return [statsCd, {}] as const;
}
const next: Record<string, string> = {};
for (const item of res.data) {
const key = item.engColNm?.trim();
const guide = item.columnGuide?.trim();
if (key && guide) next[key] = guide;
}
return [statsCd, next] as const;
} catch {
return [statsCd, {}] as const;
}
}),
);
const next = await fetchColumnGuidesRecord(statsCd);
if (cancelled) return;
const guidesByStatsCd = Object.fromEntries(entries);
setColumnGuidesByTab(
Object.fromEntries(
Object.entries(SEWERAGE_SERVICE_LEVEL_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY).map(
([tabKey, statsCd]) => [tabKey, guidesByStatsCd[statsCd] ?? {}],
),
),
setColumnGuidesByTab((prev) =>
Object.prototype.hasOwnProperty.call(prev, tabKey)
? prev
: { ...prev, [tabKey]: next },
);
})();
return () => {
cancelled = true;
};
}, []);
}, [activeTabId, activeTab, columnGuidesByTab]);
const commitToolbarSnapshotForLoadedTab = (
tabKey: string,
@@ -39,6 +39,8 @@ import { RecycledWaterStatusInpColumn } from "@/components/util/column/input/wat
import { TreatWaterReuseBusinessApprStatusInpColumn } from "@/components/util/column/input/waterReuse/TreatWaterReuseBusinessApprStatusInpColumn";
import { TreatWaterReuseDesignConstStatusInpColumn } from "@/components/util/column/input/waterReuse/TreatWaterReuseDesignConstStatusInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { useTabStore } from "@/store/tab-store";
import {
patchVerificationFailedByTab,
} from "@/components/util/input/verification-failed-flag";
@@ -94,14 +96,6 @@ const WaterReuseInpTabList = [
const firstTabKey = PublicSewageTreatWaterReuseInpColumn.tabKey;
const WATER_REUSE_COLUMN_GUIDE_STATS_CDS = [
WATER_REUSE_PUBLIC_SEWAGE_TREAT_STATS_CD,
WATER_REUSE_RECYCLED_WATER_STATS_CD,
WATER_REUSE_RAINFALL_UTIL_FACT_STATS_CD,
WATER_REUSE_DESIGN_CONST_STATS_CD,
WATER_REUSE_BUSINESS_APPR_STATS_CD,
] as const;
const WATER_REUSE_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY: Record<string, string> = {
[PublicSewageTreatWaterReuseInpColumn.tabKey]: WATER_REUSE_PUBLIC_SEWAGE_TREAT_STATS_CD,
[RecycledWaterStatusInpColumn.tabKey]: WATER_REUSE_RECYCLED_WATER_STATS_CD,
@@ -569,42 +563,29 @@ const WaterReuseInp = () => {
const loadedTabKeysRef = useRef<Set<string>>(new Set());
activeTabRef.current = activeTab;
const activeTabId = useTabStore((s) => s.activeTabId);
useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.WATER_REUSE) return;
const tabKey = activeTab;
if (!tabKey) return;
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
const statsCd = WATER_REUSE_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY[tabKey] ?? tabKey;
let cancelled = false;
void (async () => {
const entries = await Promise.all(
WATER_REUSE_COLUMN_GUIDE_STATS_CDS.map(async (statsCd) => {
try {
const res = await statisticsApi.getColumnGuides(statsCd);
if (res.status !== "success" || !Array.isArray(res.data)) {
return [statsCd, {}] as const;
}
const next: Record<string, string> = {};
for (const item of res.data) {
const key = item.engColNm?.trim();
const guide = item.columnGuide?.trim();
if (key && guide) next[key] = guide;
}
return [statsCd, next] as const;
} catch {
return [statsCd, {}] as const;
}
}),
);
const next = await fetchColumnGuidesRecord(statsCd);
if (cancelled) return;
const guidesByStatsCd = Object.fromEntries(entries);
setColumnGuidesByTab(
Object.fromEntries(
Object.entries(WATER_REUSE_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY).map(
([tabKey, statsCd]) => [tabKey, guidesByStatsCd[statsCd] ?? {}],
),
),
setColumnGuidesByTab((prev) =>
Object.prototype.hasOwnProperty.call(prev, tabKey)
? prev
: { ...prev, [tabKey]: next },
);
})();
return () => {
cancelled = true;
};
}, []);
}, [activeTabId, activeTab, columnGuidesByTab]);
const loadSggOptions = useCallback(async (sido: string): Promise<Sgg[]> => {
if (!sido || sido === "ALL") {
@@ -12,11 +12,9 @@ import {
DialogTitle,
Input,
Label,
Textarea,
} from "@common/components";
export interface ApprovalFormData {
rjctOpnnCn: string;
lastCfmtnPicNm: string;
lastCfmtnPicTelNo: string;
lastCfmtnPicDeptNm: string;
@@ -34,7 +32,6 @@ export function ApprovalInputDialog({
onOpenChange,
onConfirm,
}: ApprovalInputDialogProps) {
const [rjctOpnnCn, setRjctOpnnCn] = useState("");
const [lastCfmtnPicNm, setLastCfmtnPicNm] = useState("");
const [lastCfmtnPicTelNo, setLastCfmtnPicTelNo] = useState("");
const [lastCfmtnPicDeptNm, setLastCfmtnPicDeptNm] = useState("");
@@ -43,7 +40,6 @@ export function ApprovalInputDialog({
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
setRjctOpnnCn("");
setLastCfmtnPicNm("");
setLastCfmtnPicTelNo("");
setLastCfmtnPicDeptNm("");
@@ -65,7 +61,6 @@ export function ApprovalInputDialog({
try {
setIsSubmitting(true);
const formData: ApprovalFormData = {
rjctOpnnCn: rjctOpnnCn.trim(),
lastCfmtnPicNm: lastCfmtnPicNm.trim(),
lastCfmtnPicTelNo: lastCfmtnPicTelNo.trim(),
lastCfmtnPicDeptNm: lastCfmtnPicDeptNm.trim(),
@@ -156,11 +156,11 @@ const GeneralVerificationManagement = () => {
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL">전체</SelectItem>
<SelectItem value="DO_SI">
도단위 검증
<SelectItem value="TOTAL">
총계 검증
</SelectItem>
<SelectItem value="SI_GU">
시단위 검증
<SelectItem value="ROW">
행 검증
</SelectItem>
</SelectContent>
</Select>
@@ -39,8 +39,8 @@ import { VerificationTargetDialog } from "./VerificationTargetDialog";
import DataGrid from "@/components/module/DataGrid";
const TAB_CONFIG = {
HB: { value: "HB", label: "H-B", columnInfo: hbArticleColumnInfo },
IMR: { value: "IMR", label: "I-MR", columnInfo: imrArticleColumnInfo },
HB: { value: "HB", label: "모집단", columnInfo: hbArticleColumnInfo },
IMR: { value: "IMR", label: "시계열", columnInfo: imrArticleColumnInfo },
GENERAL: {
value: "GENERAL",
label: "일반",
@@ -1,55 +1,93 @@
import { useEffect, useRef, useState } from 'react';
import { useEffect, useRef, useState } from "react";
import { useConfirm } from '@frontend-opensource/use-react-hooks';
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useAlertStore } from '@/shared/lib';
import { NoData } from '@/shared/ui/no-data';
import { generalverificationApiService } from '@/entities/statistics/api/general-verification-service';
import type { VerifyDetailData, VerifyDetailResponseDto } from '@/entities/statistics/model/general-verification-interface';
import { useAlertStore } from "@/shared/lib";
import { NoData } from "@/shared/ui/no-data";
import { generalverificationApiService } from "@/entities/statistics/api/general-verification-service";
import type {
VerifyDetailData,
VerifyDetailResponseDto,
} from "@/entities/statistics/model/general-verification-interface";
import {
Button,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
} from "@common/components";
import { LoadingWithAnimation } from '@common/components';
import { LoadingWithAnimation } from "@common/components";
import { getUserInfo } from '../../../../../../../packages/common/components/src/api';
import { getUserInfo } from "../../../../../../../packages/common/components/src/api";
import FomulaDialog from './FomulaDialog';
import FormulaComponents from './FormulaComponents';
import FomulaDialog from "./FomulaDialog";
import FormulaComponents from "./FormulaComponents";
type ConnectorValue = "and" | "or" | "cond";
type ClauseZone = "" | "zone-condition" | "zone-result";
const CONNECTOR_OPTIONS: { value: ConnectorValue; label: string }[] = [
{ value: "and", label: "그리고(AND)" },
{ value: "or", label: "또는(OR)" },
{ value: "cond", label: "조건연산자" },
];
interface DetailComponentProps {
selectedVrfcRow: number | null;
}
const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
const [detailInfo, setDetailInfo] = useState<VerifyDetailData | null>(null);
const [formulaList, setFormulaList] = useState<(VerifyDetailResponseDto | null)[]>([]);
const [hasPersistedDetails, setHasPersistedDetails] = useState<boolean>(false);
const [formulaList, setFormulaList] = useState<
(VerifyDetailResponseDto | null)[]
>([]);
/** UI 전용 — 수식 행 사이 연결 연산자 (and / or / 조건연산자) */
const [connectors, setConnectors] = useState<ConnectorValue[]>([]);
/** UI 전용 — 수식 행별 group by */
const [groupByFlags, setGroupByFlags] = useState<boolean[]>([]);
const [hasPersistedDetails, setHasPersistedDetails] =
useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [showFomulaDialog, setShowFomulaDialog] = useState<boolean>(false);
const [selectedFormula, setSelectedFormula] = useState<VerifyDetailResponseDto | null>(null);
const [selectedFormula, setSelectedFormula] =
useState<VerifyDetailResponseDto | null>(null);
const [selectedFormulaIndex, setSelectedFormulaIndex] = useState<number>(0);
const { confirm } = useConfirm();
const { setMessage: alert } = useAlertStore((state) => state);
const persistedFormulaSignatureRef = useRef<string>('');
const persistedFormulaSignatureRef = useRef<string>("");
const normalizeFormulaListForCompare = (list: (VerifyDetailResponseDto | null)[]) => {
const syncUiMetaForListLength = (length: number) => {
setConnectors(Array.from({ length: Math.max(0, length - 1) }, () => "and"));
setGroupByFlags(Array.from({ length }, () => false));
};
const conditionBoundary = connectors.findIndex((c) => c === "cond");
const zoneFor = (index: number): ClauseZone => {
if (conditionBoundary === -1) return "";
return index <= conditionBoundary ? "zone-condition" : "zone-result";
};
const normalizeFormulaListForCompare = (
list: (VerifyDetailResponseDto | null)[],
) => {
return list.map((row) =>
row
? {
vrfcDtlSn: row.vrfcDtlSn ?? null,
refStatsCd: row.refStatsCd ?? null,
kornArtopnEqtnCn: row.kornArtopnEqtnCn ?? '',
engArtopnEqtnCn: row.engArtopnEqtnCn ?? '',
cmprOpert: row.cmprOpert ?? null,
}
vrfcDtlSn: row.vrfcDtlSn ?? null,
refStatsCd: row.refStatsCd ?? null,
kornArtopnEqtnCn: row.kornArtopnEqtnCn ?? "",
engArtopnEqtnCn: row.engArtopnEqtnCn ?? "",
cmprOpert: row.cmprOpert ?? null,
}
: null,
);
};
@@ -57,61 +95,76 @@ const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
const handleSaveClick = async () => {
const trgtStatsCd = detailInfo?.commonInfo.trgtStatsCd;
if (!trgtStatsCd) {
alert('대상 통계항목(trgtStatsCd) 정보가 없습니다.');
alert("대상 통계항목(trgtStatsCd) 정보가 없습니다.");
return;
}
const rows = formulaList.map((row, idx) => ({
eqtnCombSeq: idx + 1,
trgtStatsCd,
refStatsCd: row?.refStatsCd ?? null,
kornArtopnEqtnCn: row?.kornArtopnEqtnCn ?? '',
engArtopnEqtnCn: row?.engArtopnEqtnCn ?? '',
kornArtopnEqtnCn: row?.kornArtopnEqtnCn ?? "",
engArtopnEqtnCn: row?.engArtopnEqtnCn ?? "",
cmprOpert: row?.cmprOpert ?? null,
}));
const UNARY_TERMINAL_OPERATORS = new Set<string>(['is_null', '!is_null', 'floor']);
const UNARY_TERMINAL_OPERATORS = new Set<string>([
"is_null",
"!is_null",
"floor",
]);
const lastOperator = rows.at(-1)?.cmprOpert ?? null;
const lastIsUnary = rows.length > 0 && lastOperator != null && UNARY_TERMINAL_OPERATORS.has(lastOperator);
const lastIsUnary =
rows.length > 0 &&
lastOperator != null &&
UNARY_TERMINAL_OPERATORS.has(lastOperator);
const isSingleUnaryFormula = rows.length === 1 && lastIsUnary;
// 단항 연산자일 때만 row 1개 허용
if (rows.length === 1 && !isSingleUnaryFormula) {
alert('해당 연산자는 최소 2개 항목이 필요합니다.');
alert("해당 연산자는 최소 2개 항목이 필요합니다.");
return;
}
// 단항 연산자(is_null, !is_null, floor)는 반드시 맨 마지막 항목에만 허용
if (rows.length > 1) {
const unaryAtWrongIdx = rows.findIndex(
(r, idx) => idx < rows.length - 1 && r.cmprOpert != null && UNARY_TERMINAL_OPERATORS.has(r.cmprOpert),
(r, idx) =>
idx < rows.length - 1 &&
r.cmprOpert != null &&
UNARY_TERMINAL_OPERATORS.has(r.cmprOpert),
);
if (unaryAtWrongIdx !== -1) {
alert(`단항 연산자(is_null, !is_null, floor)는 맨 마지막 항목에만 사용할 수 있습니다.\n(순번: ${unaryAtWrongIdx + 1})`);
alert(
`단항 연산자(is_null, !is_null, floor)는 맨 마지막 항목에만 사용할 수 있습니다.\n(순번: ${unaryAtWrongIdx + 1})`,
);
return;
}
}
const invalidIdx = rows.findIndex(
(r) => r.kornArtopnEqtnCn.trim().length === 0 || r.engArtopnEqtnCn.trim().length === 0,
(r) =>
r.kornArtopnEqtnCn.trim().length === 0 ||
r.engArtopnEqtnCn.trim().length === 0,
);
if (invalidIdx !== -1) {
alert(`수식이 비어있는 항목이 있습니다.\n(순번: ${invalidIdx + 1})`);
return;
}
// 마지막이 단항 연산자가 아닐 때만: 마지막을 제외한 항목들은 연산자(cmprOpert) 필수
if (rows.length > 1 && !lastIsUnary) {
const invalidOpertIdx = rows.slice(0, -1).findIndex((r) => r.cmprOpert == null);
const invalidOpertIdx = rows
.slice(0, -1)
.findIndex((r) => r.cmprOpert == null);
if (invalidOpertIdx !== -1) {
alert(`연산자가 선택되지 않은 항목이 있습니다.\n(순번: ${invalidOpertIdx + 1})`);
alert(
`연산자가 선택되지 않은 항목이 있습니다.\n(순번: ${invalidOpertIdx + 1})`,
);
return;
}
}
// 여러 항목일 때 마지막이 단항 연산자면:
// 이전 항목들의 연산자는 '?'일 때만 등록 허용 (자동 null 보정하지 않음)
if (rows.length > 1 && lastIsUnary) {
const invalidUnaryPrevIdx = rows.slice(0, -1).findIndex((r) => r.cmprOpert !== '?');
const invalidUnaryPrevIdx = rows
.slice(0, -1)
.findIndex((r) => r.cmprOpert !== "?");
if (invalidUnaryPrevIdx !== -1) {
alert(
`마지막 항목이 단항 연산자(is_null, !is_null, floor)일 때는 이전 항목의 연산자를 '?'로 선택해야 합니다.\n(순번: ${invalidUnaryPrevIdx + 1})`,
@@ -120,7 +173,6 @@ const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
}
}
// 마지막 항목의 연산자는 (단항 연산자 케이스 제외) 무조건 null 처리
if (rows.length > 1 && !lastIsUnary) {
const lastRow = rows[rows.length - 1];
if (lastRow) lastRow.cmprOpert = null;
@@ -135,25 +187,38 @@ const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
});
}
if (await confirm('등록하시겠습니까?')) {
if (await confirm("등록하시겠습니까?")) {
try {
const userInfo = await getUserInfo();
let response;
if (!hasPersistedDetails) {
response = await generalverificationApiService.RegistVrfcDetail({ vrfcSn: detailInfo?.commonInfo.vrfcSn ?? null, detailList: rows, rgtrId: userInfo.userUnqId });
response =
await generalverificationApiService.RegistVrfcDetail({
vrfcSn: detailInfo?.commonInfo.vrfcSn ?? null,
detailList: rows,
rgtrId: userInfo.userUnqId,
});
} else {
response = await generalverificationApiService.UpdateVrfcDetail({ vrfcSn: detailInfo?.commonInfo.vrfcSn ?? null, detailList: rows, mdfrId: userInfo.userUnqId });
response =
await generalverificationApiService.UpdateVrfcDetail({
vrfcSn: detailInfo?.commonInfo.vrfcSn ?? null,
detailList: rows,
mdfrId: userInfo.userUnqId,
});
}
if (response?.status === 'success') {
alert('등록되었습니다.');
if (response?.status === "success") {
alert("등록되었습니다.");
getVrfcDetail();
} else {
alert(response?.message || '데이터 등록 중 오류가 발생했습니다.');
alert(
response?.message ||
"데이터 등록 중 오류가 발생했습니다.",
);
}
} catch {
alert('데이터 등록 중 오류가 발생했습니다.');
alert("데이터 등록 중 오류가 발생했습니다.");
}
}
};
@@ -162,34 +227,45 @@ const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
try {
setIsLoading(true);
const response = await generalverificationApiService.getVrfcDetail(selectedVrfcRow!);
const response =
await generalverificationApiService.getVrfcDetail(
selectedVrfcRow!,
);
if (response.status === 'success') {
if (response.status === "success") {
setDetailInfo(response.data);
const list = response.data.verifyDetailResponseDtoList;
setFormulaList(list?.length ? [...list] : [null]);
const nextList = list?.length ? [...list] : [null];
setFormulaList(nextList);
syncUiMetaForListLength(nextList.length);
setHasPersistedDetails((list?.length ?? 0) > 0);
// "기존 데이터" 스냅샷(서버 응답 기준)을 시그니처로 저장
persistedFormulaSignatureRef.current = JSON.stringify(
normalizeFormulaListForCompare(list?.length ? [...list] : [null]),
normalizeFormulaListForCompare(nextList),
);
} else {
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
}
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
const handleClickVrfcDetail = (formula?: VerifyDetailResponseDto | null, index?: number) => {
const handleClickVrfcDetail = (
formula?: VerifyDetailResponseDto | null,
index?: number,
) => {
setSelectedFormula(formula ?? null);
setSelectedFormulaIndex(index ?? 0);
setShowFomulaDialog(true);
};
const handleFormulaConfirm = (payload: { kornArtopnEqtnCn: string; engArtopnEqtnCn: string; refStatsCd: string | null }) => {
const handleFormulaConfirm = (payload: {
kornArtopnEqtnCn: string;
engArtopnEqtnCn: string;
refStatsCd: string | null;
}) => {
setFormulaList((prev) => {
const next = [...prev];
const i = selectedFormulaIndex;
@@ -197,19 +273,19 @@ const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
const item = next[i];
next[i] = item
? {
...item,
kornArtopnEqtnCn: payload.kornArtopnEqtnCn,
engArtopnEqtnCn: payload.engArtopnEqtnCn,
refStatsCd: payload.refStatsCd,
}
...item,
kornArtopnEqtnCn: payload.kornArtopnEqtnCn,
engArtopnEqtnCn: payload.engArtopnEqtnCn,
refStatsCd: payload.refStatsCd,
}
: {
vrfcDtlSn: null,
refStatsCd: payload.refStatsCd,
refStatsNm: null,
kornArtopnEqtnCn: payload.kornArtopnEqtnCn,
engArtopnEqtnCn: payload.engArtopnEqtnCn,
cmprOpert: null,
};
vrfcDtlSn: null,
refStatsCd: payload.refStatsCd,
refStatsNm: null,
kornArtopnEqtnCn: payload.kornArtopnEqtnCn,
engArtopnEqtnCn: payload.engArtopnEqtnCn,
cmprOpert: null,
};
return next;
});
setShowFomulaDialog(false);
@@ -217,17 +293,50 @@ const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
};
const handleResetClick = async () => {
const currentSignature = JSON.stringify(normalizeFormulaListForCompare(formulaList));
const currentSignature = JSON.stringify(
normalizeFormulaListForCompare(formulaList),
);
if (currentSignature === persistedFormulaSignatureRef.current) {
alert('변경된 내용이 없습니다.');
alert("변경된 내용이 없습니다.");
return;
}
if (await confirm(`변경 전 데이터로 리셋하시겠습니까?\n등록 완료된 데이터는 등록 전 데이터를 불러올 수 없습니다.`)) {
if (
await confirm(
`변경 전 데이터로 리셋하시겠습니까?\n등록 완료된 데이터는 등록 전 데이터를 불러올 수 없습니다.`,
)
) {
getVrfcDetail();
}
};
const handleAddClause = () => {
setFormulaList((prev) => [...prev, null]);
setConnectors((prev) => [...prev, "and"]);
setGroupByFlags((prev) => [...prev, false]);
};
const handleRemoveClause = (index: number) => {
setFormulaList((prev) => {
const next = prev.filter((_, i) => i !== index);
return next.length > 0 ? next : [null];
});
setConnectors((prev) => {
if (prev.length === 0) return prev;
if (index < prev.length) {
return prev.filter((_, i) => i !== index);
}
if (index - 1 >= 0) {
return prev.filter((_, i) => i !== index - 1);
}
return prev;
});
setGroupByFlags((prev) => {
const next = prev.filter((_, i) => i !== index);
return next.length > 0 ? next : [false];
});
};
useEffect(() => {
if (selectedVrfcRow != null) {
getVrfcDetail();
@@ -236,20 +345,23 @@ const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
return (
<>
<div className='card card-border w-1/2 !mx-0 flex flex-col'>
<div className="card-header">
<h3 className="text-2xl font-medium text-label">검증 상세 정보</h3>
<div className="card card-border !mx-0 flex h-full min-h-0 w-1/2 flex-col">
<div className="card-header shrink-0">
<h3 className="text-2xl font-medium text-label">
검증 상세 정보
</h3>
</div>
<div className="card !m-4 h-full !p-0">
{selectedVrfcRow ?
isLoading ?
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
<div className="card !m-4 flex min-h-0 flex-1 flex-col !p-0">
{selectedVrfcRow ? (
isLoading ? (
<div className="absolute inset-0 z-50 flex items-center justify-center rounded-lg p-10 [data-theme='black']:bg-transparent">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
:
<div className='flex flex-col gap-12 m-2 h-full'>
) : (
<div className="m-2 flex min-h-0 flex-1 flex-col gap-4">
<div className="shrink-0">
<Table variant="secondary" className="table-fixed">
<colgroup>
<col width="25%" />
@@ -260,80 +372,230 @@ const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
<TableBody>
<TableRow>
<TableHead>검증명</TableHead>
<TableCell className='text-center'>{detailInfo?.commonInfo.vrfcNm ?? "-"}</TableCell>
<TableCell className="text-center">
{detailInfo?.commonInfo.vrfcNm ??
"-"}
</TableCell>
<TableHead>통계항목</TableHead>
<TableCell className='text-center'>{detailInfo?.commonInfo.trgtStatsNm ?? "-"}</TableCell>
<TableCell className="text-center">
{detailInfo?.commonInfo
.trgtStatsNm ?? "-"}
</TableCell>
</TableRow>
<TableRow>
<TableHead>검증분류</TableHead>
<TableCell className='text-center'>{detailInfo?.commonInfo.vrfcClsfNm ?? "-"}</TableCell>
<TableCell className="text-center">
{detailInfo?.commonInfo
.vrfcClsfNm ?? "-"}
</TableCell>
<TableHead>소수점처리방식</TableHead>
<TableCell className='text-center'>
{detailInfo?.commonInfo.dcptDigtCnt === 0 ?
'소수점 생략'
:
`${detailInfo?.commonInfo.dcptDigtCnt}자리에서 ${detailInfo?.commonInfo.dcptPrcsMthNm}`
}
<TableCell className="text-center">
{detailInfo?.commonInfo
.dcptDigtCnt === 0
? "소수점 생략"
: `${detailInfo?.commonInfo.dcptDigtCnt}자리에서 ${detailInfo?.commonInfo.dcptPrcsMthNm}`}
</TableCell>
</TableRow>
</TableBody>
</Table>
<div className='flex flex-col gap-4 overflow-auto'>
<h3 className="font-bold text-title">수식 정보</h3>
</div>
<div className="flex min-h-0 flex-1 flex-col overflow-auto px-1">
<div className="mb-1 font-bold text-[#33373f]">
수식 정보
</div>
<div className="mb-3.5 flex gap-4 text-[#6b7280]">
<div className="flex items-center gap-1.5">
<span className="inline-block size-3 rounded-sm bg-[#dbe8ff]" />
기준 대상
</div>
<div className="flex items-center gap-1.5">
<span className="inline-block size-3 rounded-sm bg-[#ffe9d6]" />
비교대상
</div>
</div>
{formulaList.map((data, index) => (
<FormulaComponents
key={data?.vrfcDtlSn ?? `empty-${index}`}
data={data}
index={index}
onOperatorChange={(v) => {
setFormulaList((prev) => {
const next = [...prev];
if (index < 0 || index >= next.length) return prev;
const current = next[index];
const cmprOpert = v === 'none' ? null : v;
next[index] = current
? { ...current, cmprOpert }
: {
vrfcDtlSn: null,
refStatsCd: null,
refStatsNm: null,
kornArtopnEqtnCn: '',
engArtopnEqtnCn: '',
cmprOpert,
};
return next;
});
}}
onDelete={() => {
setFormulaList((prev) => {
const next = prev.filter((_, i) => i !== index);
return next.length > 0 ? next : [null];
});
}}
handleFomulaDialog={(show, formula, idx) => {
if (show) handleClickVrfcDetail(formula, idx ?? index);
else setShowFomulaDialog(false);
}}
/>
<div key={data?.vrfcDtlSn ?? `empty-${index}`}>
<FormulaComponents
data={data}
index={index}
zone={zoneFor(index)}
groupBy={
groupByFlags[index] ?? false
}
onGroupByChange={(checked) => {
setGroupByFlags((prev) => {
const next = [...prev];
while (
next.length <=
index
) {
next.push(false);
}
next[index] = checked;
return next;
});
}}
onOperatorChange={(v) => {
setFormulaList((prev) => {
const next = [...prev];
if (
index < 0 ||
index >= next.length
) {
return prev;
}
const current =
next[index];
const cmprOpert =
v === "none"
? null
: v;
next[index] = current
? {
...current,
cmprOpert,
}
: {
vrfcDtlSn:
null,
refStatsCd:
null,
refStatsNm:
null,
kornArtopnEqtnCn:
"",
engArtopnEqtnCn:
"",
cmprOpert,
};
return next;
});
}}
onDelete={() =>
handleRemoveClause(index)
}
handleFomulaDialog={(
show,
formula,
idx,
) => {
if (show) {
handleClickVrfcDetail(
formula,
idx ?? index,
);
} else {
setShowFomulaDialog(
false,
);
}
}}
/>
{index < connectors.length && (
<div className="flex items-center justify-center gap-2 pt-1 pb-2">
<div className="h-px flex-1 bg-[#e2e5ea]" />
<Select
value={
connectors[index]
}
onValueChange={(v) => {
setConnectors(
(prev) => {
const next =
[
...prev,
];
next[
index
] =
v as ConnectorValue;
return next;
},
);
}}
>
<SelectTrigger
className={`h-[3.2rem] w-[150px] rounded-full border text-center font-semibold focus:ring-0 ${
connectors[
index
] === "cond"
? "border-[#3a5bc7] bg-[#eef3ff] text-[#3a5bc7]"
: "border-[#d8dce3] bg-white text-[#33373f]"
}`}
>
<SelectValue />
</SelectTrigger>
<SelectContent className="w-[150px]">
{CONNECTOR_OPTIONS.map(
(opt) => {
const disabled =
opt.value ===
"cond" &&
conditionBoundary !==
-1 &&
conditionBoundary !==
index;
return (
<SelectItem
key={
opt.value
}
value={
opt.value
}
disabled={
disabled
}
>
{
opt.label
}
</SelectItem>
);
},
)}
</SelectContent>
</Select>
<div className="h-px flex-1 bg-[#e2e5ea]" />
</div>
)}
</div>
))}
<div className="mt-2.5 flex justify-center">
<Button
type="button"
size="icon"
className="size-[4rem] rounded-full"
onClick={handleAddClause}
>
<i className="ri-play-list-add-fill text-[16px]" />
</Button>
</div>
<div className="mt-4 border-t border-[#eef0f3] px-1 py-3 text-[#8a8f98]">
조건연산자는 규칙 전체에서 1개만 선택할
수 있습니다. 조건연산자를 기준으로
앞쪽은 기준 대상, 뒤쪽은 비교대상으로
구분됩니다.
{conditionBoundary === -1 && (
<span className="font-semibold block text-[#f24e5b]">
{" "}
현재 조건연산자가 선택되지 않아
전체가 비교대상과 동일하게
처리됩니다.
</span>
)}
</div>
</div>
<div className='flex justify-center mb-4'>
<div className="flex shrink-0 items-center justify-end gap-2">
<Button
type="button"
size="icon"
className="size-[4rem] rounded-full"
onClick={() => {
setFormulaList((prev) => [...prev, null]);
}}
>
<i className="ri-play-list-add-fill text-[16px]"></i>
</Button>
</div>
<div className="flex-1 flex gap-2 justify-end items-end">
<Button
type="button"
color='red'
title='리셋'
color="red"
title="리셋"
onClick={handleResetClick}
>
<i className="diveicon di-arrow-reset" />
@@ -346,15 +608,20 @@ const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
</Button>
</div>
</div>
:
)
) : (
<div className="flex h-full items-center justify-center">
<NoData />
</div>
}
)}
</div>
</div>
<FomulaDialog
key={showFomulaDialog ? (selectedFormula?.vrfcDtlSn ?? 'new') : 'closed'}
key={
showFomulaDialog
? (selectedFormula?.vrfcDtlSn ?? "new")
: "closed"
}
open={showFomulaDialog}
onOpenChange={(open) => {
setShowFomulaDialog(open);
@@ -369,4 +636,4 @@ const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
);
};
export default DetailComponent;
export default DetailComponent;
@@ -1,41 +1,83 @@
import { Button, Input } from "@common/components";;
import OperatorsComponents from "./OperatorsComponents";
import { Button, Input } from "@common/components";
import type { VerifyDetailResponseDto } from "@/entities/statistics/model/general-verification-interface";
import OperatorsComponents from "./OperatorsComponents";
type ClauseZone = "" | "zone-condition" | "zone-result";
interface FormulaComponentsProps {
data?: VerifyDetailResponseDto | null;
index?: number;
zone?: ClauseZone;
groupBy?: boolean;
onGroupByChange?: (checked: boolean) => void;
onDelete?: () => void;
onOperatorChange?: (value: string) => void;
handleFomulaDialog?: (show: boolean, data?: VerifyDetailResponseDto | null, index?: number) => void;
handleFomulaDialog?: (
show: boolean,
data?: VerifyDetailResponseDto | null,
index?: number,
) => void;
}
const FormulaComponents = ({ data, index, onDelete, onOperatorChange, handleFomulaDialog }: FormulaComponentsProps) => {
const zoneClassName: Record<ClauseZone, string> = {
"": "bg-[#f9fafb]",
"zone-condition": "bg-[#eef4ff]",
"zone-result": "bg-[#fff4ea]",
};
const FormulaComponents = ({
data,
index,
zone = "",
groupBy = false,
onGroupByChange,
onDelete,
onOperatorChange,
handleFomulaDialog,
}: FormulaComponentsProps) => {
const cmprOpert = data?.cmprOpert;
return (
<div className='flex items-center gap-x-4 mb-2 p-2 justify-around rounded has-[.formulaDeleteBtn:hover]:bg-gray-100'>
<div
className={`mb-1.5 flex items-center gap-2.5 rounded-md px-2.5 py-2 ${zoneClassName[zone]}`}
>
<Button
type="button"
size="icon"
className="size-[3rem] shrink-0 rounded-full formulaDeleteBtn"
color='red'
className="formulaDeleteBtn size-[2.6rem] shrink-0 rounded-full !bg-[#f24e5b] hover:!bg-[#d63c48]"
color="red"
onClick={onDelete}
>
<i className="ri-subtract-fill text-[16px]"></i>
<i className="ri-subtract-fill text-[16px]" />
</Button>
<Input
className='h-[4rem] !cursor-pointer'
placeholder='클릭하여 수식을 등록해주세요.'
id='formulaInput'
value={data ? data.kornArtopnEqtnCn : ''}
title={data ? data.kornArtopnEqtnCn : ''}
className="h-[3.6rem] flex-1 !cursor-pointer rounded-md border-[#d8dce3] text-[13px]"
placeholder="클릭하여 수식을 등록해주세요."
id={`formulaInput-${index ?? 0}`}
value={data ? data.kornArtopnEqtnCn : ""}
title={data ? data.kornArtopnEqtnCn : ""}
onClick={() => handleFomulaDialog?.(true, data, index)}
readOnly
/>
<OperatorsComponents cmprOpert={cmprOpert} onValueChange={onOperatorChange} />
<div className="w-[200px] shrink-0 [&_button]:!h-[3.6rem] [&_button]:!w-[200px] [&_button]:min-w-[200px] [&_button]:max-w-[200px]">
<OperatorsComponents
cmprOpert={cmprOpert}
onValueChange={onOperatorChange}
/>
</div>
<label className="flex w-[96px] shrink-0 items-center justify-end gap-1.5 text-[#4b5563] cursor-pointer">
<input
type="checkbox"
className="cursor-pointer accent-[#3a6df0]"
checked={groupBy}
onChange={(e) => onGroupByChange?.(e.target.checked)}
/>
group by
</label>
</div>
);
};
export default FormulaComponents;
export default FormulaComponents;
@@ -384,9 +384,8 @@ const RegistDialog = memo(
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL">공통 검증</SelectItem>
<SelectItem value="DO_SI">도단위 검증</SelectItem>
<SelectItem value="SI_GU">시단위 검증</SelectItem>
<SelectItem value="TOTAL">총계 검증</SelectItem>
<SelectItem value="ROW">행 검증</SelectItem>
</SelectContent>
</Select>
</FormControl>

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