This commit is contained in:
@@ -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);
|
||||
}}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user