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

This commit is contained in:
2026-10-02 15:16:46 +09:00
15 changed files with 82 additions and 19 deletions
@@ -12,11 +12,11 @@ export const designCnstnRegistSchema = z.object({
bizBgngYr: z
.string()
.min(1, "사업시작연도는 필수입니다")
.regex(/^\d{4}$/, "사업시작연도는 4자리 숫자여야 합니다"),
.regex(/^\d{4}$/, "연도는 4자리 숫자여야 합니다"),
bizEndYr: z
.string()
.min(1, "사업종료연도는 필수입니다")
.regex(/^\d{4}$/, "사업종료연도는 4자리 숫자여야 합니다"),
.regex(/^\d{4}$/, "연도는 4자리 숫자여야 합니다"),
bizCn: z.string().min(1, "사업내용은 필수입니다"),
taskSeComCd: z.array(z.string()).min(1, "구분은 필수입니다"),
dsgnrNm: z.string().min(1, "설계자 이름은 필수입니다"),
@@ -3,6 +3,7 @@
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { NON_NEGATIVE_INPUT_PROPS } from "@/shared/lib";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldError, FieldGroup } from "@/shared/ui/field";
import { findCodeLabel, useCodeList } from "@/entities/code";
@@ -82,6 +83,7 @@ function BasicInfoFormSection() {
<Field data-invalid={fieldState.invalid} className="w-auto !gap-1">
<Input
{...field}
{...NON_NEGATIVE_INPUT_PROPS}
type="number"
id={field.name}
placeholder="시작연도"
@@ -100,6 +102,7 @@ function BasicInfoFormSection() {
<Field data-invalid={fieldState.invalid} className="w-auto !gap-1">
<Input
{...field}
{...NON_NEGATIVE_INPUT_PROPS}
type="number"
id={field.name}
placeholder="종료연도"
@@ -16,11 +16,11 @@ export const fnrsCnsltnRegistSchema = z.object({
bizBgngYr: z
.string()
.min(1, "사업시작연도는 필수입니다")
.regex(/^\d{4}$/, "사업시작연도는 4자리 숫자여야 합니다"),
.regex(/^\d{4}$/, "연도는 4자리 숫자여야 합니다"),
bizEndYr: z
.string()
.min(1, "사업종료연도는 필수입니다")
.regex(/^\d{4}$/, "사업종료연도는 4자리 숫자여야 합니다"),
.regex(/^\d{4}$/, "연도는 4자리 숫자여야 합니다"),
bizCn: z.string().min(1, "사업내용은 필수입니다"),
taskSeComCd: z.array(z.string()).min(1, "구분은 필수입니다"),
dsgnrNm: z.string().min(1, "설계자 이름은 필수입니다"),
@@ -3,6 +3,7 @@
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { NON_NEGATIVE_INPUT_PROPS } from "@/shared/lib";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from "@/shared/ui/field";
import { findCodeLabel, useCodeList } from "@/entities/code";
@@ -92,9 +93,10 @@ function BasicInfoFormSection() {
name="fnrsCnsltnMng.bizBgngYr"
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid} className="w-auto">
<Field data-invalid={fieldState.invalid} className="w-auto !gap-1">
<Input
{...field}
{...NON_NEGATIVE_INPUT_PROPS}
type="number"
id={field.name}
placeholder="시작연도"
@@ -110,9 +112,10 @@ function BasicInfoFormSection() {
name="fnrsCnsltnMng.bizEndYr"
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid} className="w-auto">
<Field data-invalid={fieldState.invalid} className="w-auto !gap-1">
<Input
{...field}
{...NON_NEGATIVE_INPUT_PROPS}
type="number"
id={field.name}
placeholder="종료연도"
@@ -15,11 +15,11 @@ export const instlSncnRegistSchema = z.object({
bizBgngYr: z
.string()
.min(1, "사업시작연도는 필수입니다")
.regex(/^\d{4}$/, "사업시작연도는 4자리 숫자여야 합니다"),
.regex(/^\d{4}$/, "연도는 4자리 숫자여야 합니다"),
bizEndYr: z
.string()
.min(1, "사업종료연도는 필수입니다")
.regex(/^\d{4}$/, "사업종료연도는 4자리 숫자여야 합니다"),
.regex(/^\d{4}$/, "연도는 4자리 숫자여야 합니다"),
bizCn: z.string().min(1, "사업내용은 필수입니다"),
taskSeComCd: z.array(z.string()).min(1, "구분은 필수입니다"),
sncnSeComCd: z.string().min(1, "사업구분은 필수입니다"),
@@ -3,6 +3,7 @@
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { NON_NEGATIVE_INPUT_PROPS } from "@/shared/lib";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from "@/shared/ui/field";
import { findCodeLabel, useCodeList } from "@/entities/code";
@@ -96,9 +97,10 @@ function BasicInfoFormSection() {
name="instlSncnMng.bizBgngYr"
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid} className="w-auto">
<Field data-invalid={fieldState.invalid} className="w-auto !gap-1">
<Input
{...field}
{...NON_NEGATIVE_INPUT_PROPS}
type="number"
id={field.name}
placeholder="시작연도"
@@ -114,9 +116,10 @@ function BasicInfoFormSection() {
name="instlSncnMng.bizEndYr"
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid} className="w-auto">
<Field data-invalid={fieldState.invalid} className="w-auto !gap-1">
<Input
{...field}
{...NON_NEGATIVE_INPUT_PROPS}
type="number"
id={field.name}
placeholder="종료연도"
@@ -3,6 +3,7 @@
import { useEffect, useMemo } from "react";
import { useFormContext, useWatch } from "react-hook-form";
import { NON_NEGATIVE_INPUT_PROPS, stripMinusSign } from "@/shared/lib";
import { SectionCollapsible } from "@/shared/ui";
import { Field } from "@/shared/ui/field";
import { useAcntgSeMngTree } from "@/entities/acntg";
@@ -50,7 +51,7 @@ function BizCstMatrixFormSection() {
];
function handleChange(acntgSeCd: string, planStpNo: number, value: string) {
const normalized = truncateDecimals(value, 3);
const normalized = truncateDecimals(stripMinusSign(value), 3);
const numValue = Number(normalized.replace(/,/g, "")) || 0;
const next = recalculateBizCstMatrix(
setAmt(getValues("bizCstList"), acntgSeCd, planStpNo, numValue),
@@ -113,6 +114,7 @@ function BizCstMatrixFormSection() {
<TableCell key={step.planStpNo}>
<Field>
<Input
{...NON_NEGATIVE_INPUT_PROPS}
type="number"
step={0.001}
value={getAmt(bizCstList, item.code, step.planStpNo) || ""}
@@ -3,6 +3,7 @@
import { useMemo } from "react";
import { useFormContext, useWatch } from "react-hook-form";
import { NON_NEGATIVE_INPUT_PROPS, stripMinusSign } from "@/shared/lib";
import { SectionCollapsible } from "@/shared/ui";
import { Field } from "@/shared/ui/field";
import { useAcntgSeMngTree } from "@/entities/acntg";
@@ -58,7 +59,7 @@ function FnrsCstFormSection() {
);
function handleChange(planStpNo: number, fieldKey: EditableFnrsKey, value: string) {
const numValue = Number(value.replace(/,/g, "")) || 0;
const numValue = Number(stripMinusSign(value).replace(/,/g, "")) || 0;
const draft = [...getValues("fnrsCstList")];
const index = draft.findIndex((item) => item.planStpNo === planStpNo);
if (index < 0) return;
@@ -154,6 +155,7 @@ function FnrsCstFormSection() {
<TableCell key={planStpNo}>
<Field>
<Input
{...NON_NEGATIVE_INPUT_PROPS}
type="number"
className={cn("text-right", row.readonly && "bg-muted")}
readOnly={row.readonly}
@@ -4,6 +4,7 @@ import { useState } from "react";
import { useFormContext, useWatch } from "react-hook-form";
import dynamic from "next/dynamic";
import { NON_NEGATIVE_INPUT_PROPS, stripMinusSign } from "@/shared/lib";
import { SectionCollapsible } from "@/shared/ui";
import { Field } from "@/shared/ui/field";
import { truncateDecimals } from "@/widgets/pbadms-sprt";
@@ -61,7 +62,8 @@ function OutlineFormSection() {
value: string,
decimals?: number,
) {
const normalized = decimals === undefined ? value : truncateDecimals(value, decimals);
const unsigned = stripMinusSign(value);
const normalized = decimals === undefined ? unsigned : truncateDecimals(unsigned, decimals);
const numValue = Number(normalized.replace(/,/g, "")) || 0;
const draft = [...getValues("sewerBscPlanOtlnList")];
const index = draft.findIndex((item) => item.planStpNo === planStpNo);
@@ -107,6 +109,7 @@ function OutlineFormSection() {
<TableCell key={planStpNo}>
<Field>
<Input
{...NON_NEGATIVE_INPUT_PROPS}
type="number"
step={row.step}
className="text-right"
@@ -5,7 +5,12 @@ import { Controller, useFieldArray, useFormContext, useWatch } from "react-hook-
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { getDefaultFileKey, useTus } from "@/shared/lib";
import {
getDefaultFileKey,
NON_NEGATIVE_INPUT_PROPS,
stripMinusSign,
useTus,
} from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { Field } from "@/shared/ui/field";
@@ -95,13 +100,14 @@ function TmplntDtlInput({ facilityIndex, planStpNo, fieldKey, decimals }: Tmplnt
render={({ field }) => (
<Field>
<Input
{...NON_NEGATIVE_INPUT_PROPS}
type="number"
inputMode="decimal"
step={hasDecimals ? Number(`1e-${decimals}`) : undefined}
className="text-right"
value={field.value ?? ""}
onChange={(e) => {
const value = e.target.value.replace(/,/g, "");
const value = stripMinusSign(e.target.value.replace(/,/g, ""));
const normalized = hasDecimals ? truncateDecimals(value, decimals) : value;
field.onChange(Number(normalized) || 0);
}}
+1
View File
@@ -2,6 +2,7 @@ export { getCookie, removeCookie, setCookie } from "./cookie";
export { dayjs } from "./dayjs";
export { getListHref, withListQs } from "./list-return-href";
export { matchQueryStatus } from "./match-query-status";
export { NON_NEGATIVE_INPUT_PROPS, stripMinusSign } from "./number-input";
export { queryClient } from "./query-client";
export { useAlertStore } from "./use-alert-store";
export { useDataTable } from "./use-data-table";
+13
View File
@@ -0,0 +1,13 @@
import type { KeyboardEvent } from "react";
function preventMinusKey(e: KeyboardEvent<HTMLInputElement>) {
if (e.key === "-") e.preventDefault();
}
/** type="number" Input에 스프레드해 음수 입력(키·스피너)을 막는다 */
export const NON_NEGATIVE_INPUT_PROPS = { min: 0, onKeyDown: preventMinusKey } as const;
/** 붙여넣기 등으로 들어온 음수 부호를 제거한다 */
export function stripMinusSign(value: string): string {
return value.replace(/-/g, "");
}
@@ -3,6 +3,7 @@
import { useEffect, useMemo } from "react";
import { useFormContext, useWatch } from "react-hook-form";
import { NON_NEGATIVE_INPUT_PROPS, stripMinusSign } from "@/shared/lib";
import { SectionCollapsible } from "@/shared/ui";
import { Field } from "@/shared/ui/field";
import { useAcntgSeMngTree } from "@/entities/acntg";
@@ -79,7 +80,7 @@ function BusinessCostFormSection() {
}, [costItems, getValues, setValue]);
function handleCostChange(index: number, fieldKey: CostFieldKey, value: string) {
const numValue = Number(truncateDecimals(value, COST_DECIMAL_PLACES)) || 0;
const numValue = Number(truncateDecimals(stripMinusSign(value), COST_DECIMAL_PLACES)) || 0;
const draft = [...getValues("bizCstList")];
draft[index] = { ...draft[index], [fieldKey]: numValue };
const next = recalculateBizCstTotals(draft, costItems);
@@ -102,6 +103,14 @@ function BusinessCostFormSection() {
<div className="overflow-x-auto">
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
<Table variant="secondary" className="table-fixed">
<colgroup>
<col width="20%" />
<col width="20%" />
<col width="20%" />
<col width="20%" />
<col width="10%" />
<col width="10%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead className="w-[30rem]" rowSpan={2}>
@@ -143,6 +152,7 @@ function BusinessCostFormSection() {
<TableCell key={fieldKey}>
<Field>
<Input
{...NON_NEGATIVE_INPUT_PROPS}
type="number"
step={COST_STEP}
value={data[fieldKey] ?? ""}
@@ -3,6 +3,7 @@
import { useEffect, useMemo } from "react";
import { useFormContext, useWatch } from "react-hook-form";
import { NON_NEGATIVE_INPUT_PROPS, stripMinusSign } from "@/shared/lib";
import { SectionCollapsible } from "@/shared/ui";
import { Field } from "@/shared/ui/field";
import { useCnstpSeMngTree } from "@/entities/cnstp";
@@ -77,7 +78,8 @@ function BusinessScaleFormSection() {
function handleScaleChange(index: number, fieldKey: ScaleFieldKey, value: string) {
const item = scaleItems[index];
const normalized = item?.numYn ? truncateDecimals(value, item.dcptUnitCnt) : value;
const unsigned = stripMinusSign(value);
const normalized = item?.numYn ? truncateDecimals(unsigned, item.dcptUnitCnt) : unsigned;
const numValue = Number(normalized) || 0;
const draft = [...getValues("bzScaleList")];
draft[index] = { ...draft[index], [fieldKey]: numValue };
@@ -100,6 +102,13 @@ function BusinessScaleFormSection() {
<SectionCollapsible title="사업규모">
<div className="overflow-x-auto">
<Table variant="secondary">
<colgroup>
<col width="20%" />
<col width="20%" />
<col width="20%" />
<col width="20%" />
<col width="20%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead className="w-[30rem]">구분</TableHead>
@@ -132,6 +141,7 @@ function BusinessScaleFormSection() {
<TableCell key={fieldKey}>
<Field>
<Input
{...(item.numYn ? NON_NEGATIVE_INPUT_PROPS : {})}
type={item.numYn ? "number" : "text"}
step={item.numYn ? item.step : undefined}
value={data[fieldKey] ?? ""}
@@ -3,7 +3,12 @@
import { useEffect, useMemo, useState } from "react";
import { useFormContext, useWatch } from "react-hook-form";
import { getDefaultFileKey, useTus } from "@/shared/lib";
import {
getDefaultFileKey,
NON_NEGATIVE_INPUT_PROPS,
stripMinusSign,
useTus,
} from "@/shared/lib";
import { AttachmentFileField, SectionCollapsible } from "@/shared/ui";
import { Field } from "@/shared/ui/field";
import { useAcntgSeMngTree } from "@/entities/acntg";
@@ -142,7 +147,7 @@ function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsShar
if (fieldKey === "rmrkCn") {
draft[index] = { ...row, rmrkCn: value };
} else {
const numValue = Number(value) || 0;
const numValue = Number(stripMinusSign(value)) || 0;
draft[index] = { ...row, [fieldKey]: numValue };
}
@@ -236,6 +241,7 @@ function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsShar
<TableCell>
<Field>
<Input
{...NON_NEGATIVE_INPUT_PROPS}
type="number"
value={data.exstnAmt ?? ""}
onChange={(e) => handleSharChange(idx, "exstnAmt", e.target.value)}
@@ -247,6 +253,7 @@ function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsShar
<TableCell>
<Field>
<Input
{...NON_NEGATIVE_INPUT_PROPS}
type="number"
value={data.chgRqtAmt ?? ""}
onChange={(e) => handleSharChange(idx, "chgRqtAmt", e.target.value)}