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

This commit is contained in:
2026-08-27 17:25:06 +09:00
6 changed files with 103 additions and 32 deletions
@@ -141,6 +141,7 @@ const verificationResultColumnInfo: ColumnInfo = {
columns: [
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역" }, editable: false },
{ name: "fcltNm", fieldName: "fcltNm", header: { text: "시설명" }, editable: false },
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
@@ -157,6 +158,7 @@ const verificationResultColumnInfo: ColumnInfo = {
columnLayout: [
"ctpvNm",
"admdstNm",
"fcltNm",
"vrfcTrgtNm",
"rsnExpln",
"nowVlCn",
@@ -178,7 +180,7 @@ export interface VerificationResultHistoryDialogProps {
queryContext?: {
sewerBzmnCd: string;
ctpvNm: string;
sggNm: string;
sggNm?: string;
statsYr: string;
statsCd: string;
} | null;
@@ -378,8 +380,9 @@ export function VerificationResultHistoryDialog({
return {
vrfcSn: item.vrfcSn,
__checked: isChecked ? 1 : 0,
ctpvNm: item.ctpvNm,
ctpvNm: item.ctpvNm ?? "-",
admdstNm: item.admdstNm ?? item.sggNm ?? "-",
fcltNm: item.fcltNm ?? "-",
vrfcTrgtNm: item.vrfcTrgtNm,
rsnExpln: item.rsnExpln,
nowVlCn: item.nowVlCn ?? "-",
@@ -562,7 +565,7 @@ export function VerificationResultHistoryDialog({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[80%]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>{`${title} - ${queryContext?.statsYr} ${queryContext?.ctpvNm} ${queryContext?.sggNm}`}</DialogTitle>
<DialogTitle>{`${title} - ${queryContext?.statsYr} ${queryContext?.ctpvNm} ${queryContext?.sggNm ? queryContext?.sggNm : ''}`}</DialogTitle>
</DialogHeader>
<div className="max-h-[85vh] space-y-5 overflow-auto">
{errorMessage && (
@@ -632,6 +632,7 @@ const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
kornArtopnEqtnCn: string;
engArtopnEqtnCn: string;
refStatsCd: string | null;
refStatsNm: string | null;
}) => {
setRuleBlocks((prev) => {
const i = prev.findIndex(
@@ -650,11 +651,12 @@ const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
kornArtopnEqtnCn: payload.kornArtopnEqtnCn,
engArtopnEqtnCn: payload.engArtopnEqtnCn,
refStatsCd: payload.refStatsCd,
refStatsNm: payload.refStatsNm,
}
: {
vrfcDtlSn: null,
refStatsCd: payload.refStatsCd,
refStatsNm: null,
refStatsNm: payload.refStatsNm,
kornArtopnEqtnCn: payload.kornArtopnEqtnCn,
engArtopnEqtnCn: payload.engArtopnEqtnCn,
cmprOpert: null,
@@ -1034,6 +1036,7 @@ const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
);
}
}}
vrfcClsfCd={detailInfo?.commonInfo.vrfcClsfCd}
/>
</div>
);
@@ -1,9 +1,12 @@
import { useEffect, useRef, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { useAlertStore } from "@/shared/lib";
import { CodeSelect } from "@/entities/code";
import { ENDPOINT } from "@/shared/config";
import { CodeApi } from "@/entities/code";
import { generalverificationApiService } from "@/entities/statistics/api/general-verification-service";
import type { SelectedVerifyColumnListResponseData,VerifyColumnListResponseData, VerifyDetailResponseDto } from "@/entities/statistics/model/general-verification-interface";
import type { SelectedVerifyColumnListResponseData, VerifyColumnListResponseData, VerifyDetailResponseDto } from "@/entities/statistics/model/general-verification-interface";
import {
Button,
Checkbox,
@@ -14,6 +17,11 @@ import {
DialogHeader,
DialogTitle,
Input,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Skeleton,
Textarea,
} from "@common/components";
@@ -21,8 +29,10 @@ import {
export interface FomulaDialogConfirmPayload {
kornArtopnEqtnCn: string;
engArtopnEqtnCn: string;
/** 참조 통계코드 (선택 안 했으면 null) */
/** 참조 통계코드 (수식에 참조 컬럼이 없으면 null) */
refStatsCd: string | null;
/** 참조 통계명 (수식에 참조 컬럼이 없으면 null) */
refStatsNm: string | null;
}
interface FomulaDialogProps {
@@ -63,6 +73,11 @@ const FomulaDialog = ({ open, onOpenChange, data, trgtStatsCd, trgtStatsNm, allo
const isUserDirectInputMode = allowLikeDirectInput && userDirectInput;
const { setMessage: alert } = useAlertStore((state) => state);
const { data: statsItemCodes } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "STITEM"],
queryFn: () => CodeApi.getCode("STITEM"),
enabled: open,
});
/** 다이얼로그에서 쓰는 기호 → 제출용 value. 백엔드 규격에 맞게 수정 가능 */
const OPERATOR_SYMBOL_TO_VALUE: Record<string, string> = {
@@ -375,6 +390,7 @@ const FomulaDialog = ({ open, onOpenChange, data, trgtStatsCd, trgtStatsNm, allo
const handleChangeRefStatsCd = (next: string) => {
if (!open) return;
if (trgtStatsCd && next === trgtStatsCd) return;
// 현재 refStatsCdLocal에 해당하는 참조 컬럼이 수식에 포함돼 있으면 변경을 막고 안내만 표시
const currentRefCols = refColList ?? [];
@@ -404,6 +420,26 @@ const FomulaDialog = ({ open, onOpenChange, data, trgtStatsCd, trgtStatsNm, allo
/** 참조 컬럼: 선택한 참조 통계로 조회한 목록이 있으면 사용, 없으면 getVerifyColumnList 결과 사용 */
const refColList = selectedColumnInfo?.targetColumnInfo?.colList ?? columnInfo?.refColumnInfo?.colList ?? [];
const formulaUsesRefColumn = (): boolean => {
const engSet = new Set(refColList.map((c) => c.engColNm).filter(Boolean));
if (engSet.size === 0) return false;
return formulaValueSegments.some((seg) => engSet.has(seg));
};
const resolveRefStatsForConfirm = (isEmptyFormula: boolean): {
refStatsCd: string | null;
refStatsNm: string | null;
} => {
if (isEmptyFormula || !formulaUsesRefColumn() || !refStatsCdLocal) {
return { refStatsCd: null, refStatsNm: null };
}
const refStatsNm =
statsItemCodes?.find((item) => item.lwpoCd === refStatsCdLocal)?.lwpoCdNm
?? data?.refStatsNm
?? null;
return { refStatsCd: refStatsCdLocal, refStatsNm };
};
const handleOpenChange = (nextOpen: boolean) => {
if (!nextOpen) {
setFormulaDisplaySegments([]);
@@ -528,11 +564,10 @@ const FomulaDialog = ({ open, onOpenChange, data, trgtStatsCd, trgtStatsNm, allo
)}
</div>
<Textarea
className={`w-full flex-1 resize-none !text-3xl p-4 !leading-loose font-bold ${
isUserDirectInputMode
className={`w-full flex-1 resize-none !text-3xl p-4 !leading-loose font-bold ${isUserDirectInputMode
? "bg-white text-gray-900"
: "bg-gray-100 text-gray-600"
}`}
}`}
id="formulaDisplayInput"
ref={formulaTextareaRef}
value={formulaDisplaySegments.join('')}
@@ -648,13 +683,25 @@ const FomulaDialog = ({ open, onOpenChange, data, trgtStatsCd, trgtStatsNm, allo
<div className="flex items-center justify-between">
<div className="flex gap-2 justify-center items-center">
<h3 className="font-bold text-title text-[14px]">참조 컬럼</h3>
<CodeSelect
upCd="STITEM"
<Select
value={refStatsCdLocal}
onValueChange={handleChangeRefStatsCd}
placeholder="선택"
className="w-[16rem]"
/>
>
<SelectTrigger className="w-[16rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{statsItemCodes?.map((item) => (
<SelectItem
key={item.lwpoCd}
value={item.lwpoCd}
disabled={Boolean(trgtStatsCd) && item.lwpoCd === trgtStatsCd}
>
{item.lwpoCdNm}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{(refColList?.length ?? 0) > 0 ?
<Button
@@ -717,14 +764,11 @@ const FomulaDialog = ({ open, onOpenChange, data, trgtStatsCd, trgtStatsNm, allo
const raw = formulaDisplaySegments.join("");
const wrapped = wrapWithBacktick(raw);
const isEmptyFormula = raw.trim().length === 0;
const refStats = resolveRefStatsForConfirm(isEmptyFormula);
onConfirm?.({
kornArtopnEqtnCn: isEmptyFormula ? raw : wrapped,
engArtopnEqtnCn: isEmptyFormula ? raw : wrapped,
refStatsCd: isEmptyFormula
? null
: refStatsCdLocal
? refStatsCdLocal
: null,
...refStats,
});
onOpenChange?.(false);
return;
@@ -742,10 +786,11 @@ const FomulaDialog = ({ open, onOpenChange, data, trgtStatsCd, trgtStatsNm, allo
const kornArtopnEqtnCn = formulaDisplaySegments.join('');
const engArtopnEqtnCn = getFormulaValueForSubmit();
const isEmptyFormula = kornArtopnEqtnCn.trim().length === 0;
const refStats = resolveRefStatsForConfirm(isEmptyFormula);
onConfirm?.({
kornArtopnEqtnCn,
engArtopnEqtnCn,
refStatsCd: isEmptyFormula ? null : (refStatsCdLocal ? refStatsCdLocal : null),
...refStats,
});
onOpenChange?.(false);
}}
@@ -18,6 +18,7 @@ interface FormulaComponentsProps {
data?: VerifyDetailResponseDto | null,
index?: number,
) => void;
vrfcClsfCd?: string;
}
const zoneClassName: Record<ClauseZone, string> = {
@@ -43,6 +44,7 @@ const FormulaComponents = ({
onDelete,
onOperatorChange,
handleFomulaDialog,
vrfcClsfCd,
}: FormulaComponentsProps) => {
const cmprOpert = data?.cmprOpert;
const displayFormula = data
@@ -77,15 +79,17 @@ const FormulaComponents = ({
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>
{vrfcClsfCd !== "TOTAL" &&
<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>
);
};
@@ -58,6 +58,7 @@ import {
export class StatisticsApiService implements StatisticsService {
public async getSidoList(params?: {
instSeCd?: string;
isInput?: boolean;
}): Promise<ApiResponse<SearchCondition>> {
const response = await stClient.get<ApiResponse<SearchCondition>>(
`${ENDPOINT.ST_SERVICE.COMMON}/search-condition`,
@@ -66,6 +67,17 @@ export class StatisticsApiService implements StatisticsService {
return response.data;
}
public async getSidoListInput(params?: {
instSeCd?: string;
isInput?: boolean;
}): Promise<ApiResponse<SearchCondition>> {
const response = await stClient.get<ApiResponse<SearchCondition>>(
`${ENDPOINT.ST_SERVICE.COMMON}/search-condition-input`,
{ params },
);
return response.data;
}
public async getSggList(
sidoName: string,
params?: { instSeCd?: string; gugunSkipYn?: "Y" | "N" },
@@ -2,6 +2,8 @@ export interface Sido {
adstDstrCd: string;
sido: string;
bsnmCd: string;
/** Y: 시군구 생략(광역시 등), N: 시군구 필요(도) */
gugunSkipYn: "Y" | "N";
}
export interface SearchCondition {
@@ -18,7 +20,7 @@ export interface Sgg {
export interface cancelSubmitRequest {
statsYr: string;
ctpvNm: string;
sggNm: string;
sggNm?: string;
sewerBzmnCd: string;
statsCd: string;
mdfrId: string;
@@ -208,6 +210,7 @@ export interface VerificationResultReportItem {
prcrId?: string | null;
pbcpAprvYn?: string;
mdfrId?: string | null;
fcltNm?: string | null;
}
export interface VerificationResultListPageInfo {
@@ -233,7 +236,7 @@ export interface SubmitExceptionRequest {
statsYr: string;
statsCd: string;
ctpvNm: string;
sggNm: string;
sggNm?: string;
sewerBzmnCd: string;
userId: string;
superAdmin?: string;
@@ -545,6 +548,7 @@ export interface unSubmitDataResponse {
export interface StatisticsService {
getSidoList(params?: { instSeCd?: string }): Promise<ApiResponse<SearchCondition>>;
getSidoListInput(params?: { instSeCd?: string }): Promise<ApiResponse<SearchCondition>>;
getSggList(sidoName: string, params?: { instSeCd?: string; gugunSkipYn?: "Y" | "N" }): Promise<ApiResponse<Sgg[]>>;
getSggAllList(sidoName: string, params?: { instSeCd?: string; gugunSkipYn?: "Y" | "N" }): Promise<ApiResponse<Sgg[]>>;
getStatsInputPeriodList(params?: StatsInputPeriodListRequest): Promise<StatsInputPeriodListApiResponse>;