426 lines
12 KiB
TypeScript
426 lines
12 KiB
TypeScript
import { lwCds, type comCodeState } from "@/app/store/common/code";
|
|
import {
|
|
filePickedBlob,
|
|
filePickedBlobArray,
|
|
filePickedBlobNumStyle,
|
|
type ReturnType,
|
|
type SelectDataType,
|
|
type SheetParameter,
|
|
} from "@common/business_components/ui";
|
|
import { Button } from "@common/components/ui";
|
|
import moment from "moment";
|
|
import type { RefObject } from "react";
|
|
import toast from "react-hot-toast";
|
|
|
|
export const getCity = (val: string, sggCd: SelectDataType[]) => {
|
|
let data: any;
|
|
if (val !== "0") {
|
|
data = sggCd
|
|
.filter((item) => String(item.val).substring(0, 2) === val)
|
|
.map((item) => ({
|
|
val: item.val,
|
|
text: item.text, // 한국어 텍스트
|
|
}));
|
|
} else {
|
|
data = sggCd.map((item) => ({
|
|
val: item.val,
|
|
text: item.text, // 한국어 텍스트
|
|
}));
|
|
}
|
|
data.unshift({ val: "0", text: "전체" });
|
|
return data;
|
|
};
|
|
|
|
// cd text 넣기
|
|
export const tableData = (data: any, code: comCodeState) => {
|
|
const chCode = code.code;
|
|
return data?.map(
|
|
(item: {
|
|
sggCd: string;
|
|
ctpvCd: string;
|
|
reuseBuilding: {
|
|
lawTrgtYn: string;
|
|
bdstUsgCd: string;
|
|
grntchBdstCertGrd: string;
|
|
}[];
|
|
}) => {
|
|
let returnData = {};
|
|
const sggText = chCode.sggCd.find(
|
|
(cd) => cd.val === String(item.sggCd).trim()
|
|
)?.text;
|
|
const cityText = chCode.cityCd.find(
|
|
(cd) => cd.val === String(item.ctpvCd).trim()
|
|
)?.text;
|
|
if (item?.reuseBuilding?.length > 0) {
|
|
const bdsgUsgText = chCode?.bdstTotalUsgType?.find(
|
|
(cd) => cd.val === item?.reuseBuilding[0]?.bdstUsgCd
|
|
)?.text;
|
|
|
|
const lawTrgtYn = item?.reuseBuilding[0]?.lawTrgtYn;
|
|
|
|
const grntchBdstCertGrdText = chCode?.bdstGrntchClass?.find(
|
|
(cd) => cd.val === item?.reuseBuilding[0]?.grntchBdstCertGrd
|
|
)?.text;
|
|
|
|
returnData = {
|
|
...item,
|
|
sggText: sggText ?? "없음",
|
|
cityText: cityText ?? "없음",
|
|
bdsgUsgText: bdsgUsgText, // 건물용도명
|
|
lawTrgtYn: lawTrgtYn, // 법정대상 여부
|
|
grntchBdstCertGrdText: grntchBdstCertGrdText, // 녹색건축인증등급명
|
|
};
|
|
} else {
|
|
returnData = {
|
|
...item,
|
|
sggText: sggText ?? "없음",
|
|
cityText: cityText ?? "없음",
|
|
};
|
|
}
|
|
return returnData;
|
|
}
|
|
);
|
|
};
|
|
|
|
export const excelDownload = (
|
|
excelList: [][],
|
|
fileNm: string,
|
|
sampleFile: string,
|
|
headerRange = "A1:V3",
|
|
writeLine = 4
|
|
) => {
|
|
if (excelList.length === 0) return;
|
|
fetch(`/${sampleFile}`, {}).then(async (response) => {
|
|
if (!response.ok) {
|
|
throw Error(response.statusText);
|
|
}
|
|
|
|
const isTemplateOnlyDownload =
|
|
excelList.length === 1 &&
|
|
Array.isArray(excelList[0]) &&
|
|
excelList[0].length === 0;
|
|
|
|
if (isTemplateOnlyDownload) {
|
|
const blob = await response.blob();
|
|
const link = document.createElement("a");
|
|
link.href = URL.createObjectURL(blob);
|
|
link.download = fileNm.endsWith(".xlsx") ? fileNm : `${fileNm}.xlsx`;
|
|
link.click();
|
|
URL.revokeObjectURL(link.href);
|
|
return;
|
|
}
|
|
|
|
// 받은 파일 바인너리 처리
|
|
const blobRet = await response.arrayBuffer();
|
|
// 버퍼에 있는 엑셀데이터 전달 후 json으로 변환
|
|
await filePickedBlob(blobRet, excelList, fileNm, writeLine, headerRange);
|
|
});
|
|
};
|
|
|
|
export const excelDownloadNumStyle = (
|
|
fileNm: string,
|
|
sampleFile: string,
|
|
sheetArray: SheetParameter[]
|
|
) => {
|
|
// if (excelList.length === 0) return;
|
|
fetch(`/${sampleFile}`, {}).then(async (response) => { // excel Tamplate을 읽는다.
|
|
if (!response.ok) {
|
|
throw Error(response.statusText);
|
|
}
|
|
// 받은 파일 바인너리 처리
|
|
const blobRet = await response.arrayBuffer();
|
|
// 버퍼에 있는 엑셀데이터 전달 후 json으로 변환
|
|
await filePickedBlobNumStyle(blobRet, fileNm, sheetArray);
|
|
});
|
|
};
|
|
|
|
export const excelArrayDownload = (
|
|
fileNm: string,
|
|
sampleFile: string,
|
|
sheetArray: SheetParameter[]
|
|
) => {
|
|
if (!sheetArray) return;
|
|
fetch(`/${sampleFile}`, {}).then(async (response) => {
|
|
if (!response.ok) {
|
|
throw Error(response.statusText);
|
|
}
|
|
// 받은 파일 바인너리 처리
|
|
const blobRet = await response.arrayBuffer();
|
|
// 버퍼에 있는 엑셀데이터 전달 후 json으로 변환
|
|
await filePickedBlobArray(blobRet, fileNm, sheetArray);
|
|
});
|
|
};
|
|
|
|
// onchange 텍스트를 숫자로 and ,제거
|
|
export const handleCommaNumberChange = (
|
|
e: React.ChangeEvent<HTMLInputElement>,
|
|
onChange: (value: number | "") => void
|
|
) => {
|
|
const raw = e.target.value.replace(/,/g, "");
|
|
if (!/^\d*$/.test(raw)) return;
|
|
onChange(raw === "" ? "" : Number(raw));
|
|
};
|
|
|
|
export const handleOnlyNumberChange = (
|
|
e: React.ChangeEvent<HTMLInputElement>,
|
|
onChange: (value: number | "") => void
|
|
) => {
|
|
// 콤마 제거 후 숫자와 소수점만 남기기
|
|
const cleaned = e.target.value.replace(/,/g, "").replace(/[^0-9.]/g, "");
|
|
// 숫자 및 소수점 외 입력 차단
|
|
if (!/^\d*\.?\d*$/.test(cleaned)) return;
|
|
onChange(cleaned === "" ? "" : Number(cleaned));
|
|
};
|
|
|
|
// onchange 텍스트를 숫자로 and ,제거
|
|
export const handleMinusNumberChange = (
|
|
e: React.ChangeEvent<HTMLInputElement>,
|
|
onChange: (value: number | "") => void
|
|
) => {
|
|
const raw = e.target.value.replace(/,/g, "");
|
|
if (!/^\d*$/.test(raw)) return;
|
|
onChange(raw === "" ? -1 : Number(raw));
|
|
};
|
|
|
|
// 숫자입력시 , 삽입
|
|
export const formatCommaNumber = (
|
|
value: number | string | undefined | null
|
|
): string => {
|
|
if (value === undefined || value === null || value === "") return "";
|
|
const num = Number(value);
|
|
// 숫자로 변환할 수 없는 값이 들어올 경우 빈 문자열을 반환하여 예상치 못한 "NaN" 출력을 방지합니다.
|
|
if (isNaN(num)) {
|
|
return "";
|
|
}
|
|
return num.toLocaleString();
|
|
};
|
|
|
|
//내가 쓰는 값이 숫자이냐 아니냐
|
|
export const isNumeric = (value: string) => {
|
|
if (value && value.length > 0) {
|
|
return value?.trim() !== "" && !isNaN(Number(value));
|
|
} else {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
export const getCityOrSgg = (ref: RefObject<ReturnType>) => {
|
|
let con;
|
|
if (ref.current != null)
|
|
if (ref.current.sigunValue != "0") {
|
|
con = ref.current.sigunValue;
|
|
} else {
|
|
con = ref.current.sidoValue;
|
|
}
|
|
return con;
|
|
};
|
|
|
|
export const safeDate = (value: any): Date | undefined => {
|
|
const date = new Date(value);
|
|
return isNaN(date.getTime()) ? undefined : date;
|
|
};
|
|
|
|
const currentYear = new Date().getFullYear();
|
|
export const yearOptions: { val: string; text: string }[] = Array.from(
|
|
{ length: currentYear - 1969 },
|
|
(_, i) => ({
|
|
val: String(currentYear - i),
|
|
text: `${currentYear - i}년`,
|
|
})
|
|
);
|
|
|
|
export const getdmdMatFlag = (flag: string | undefined | null) => {
|
|
const cleanFlag = (flag ?? "").trim();
|
|
|
|
switch (cleanFlag) {
|
|
case "Sub":
|
|
return "검토";
|
|
case "Matched":
|
|
return "매칭완료";
|
|
case "Checked":
|
|
return "검토완료";
|
|
default:
|
|
return "신청";
|
|
}
|
|
};
|
|
|
|
export const formatPhoneNumber = (tel: string) => {
|
|
const onlyNums = tel.replace(/[^0-9]/g, "");
|
|
if (onlyNums.length === 10) {
|
|
return onlyNums.replace(/(\d{2})(\d{4})(\d{4})/, "$1-$2-$3");
|
|
} else if (onlyNums.length === 11) {
|
|
return onlyNums.replace(/(\d{3})(\d{4})(\d{4})/, "$1-$2-$3");
|
|
} else {
|
|
return tel; // 유효하지 않으면 빈 문자열
|
|
}
|
|
};
|
|
|
|
export const confirmToast = (
|
|
content: string,
|
|
saveFunc: () => void,
|
|
contentClass?: string
|
|
) => {
|
|
toast(
|
|
(t) => (
|
|
<span className="flex flex-col items-center">
|
|
<div className={`mb-6 ${contentClass}`}>{content}</div>
|
|
<div className="flex flex-row space-x-2">
|
|
<Button
|
|
size={"sm"}
|
|
onClick={() => {
|
|
saveFunc();
|
|
toast.dismiss(t.id);
|
|
}}
|
|
>
|
|
확인
|
|
</Button>
|
|
<Button size={"sm"} onClick={() => toast.dismiss(t.id)}>
|
|
취소
|
|
</Button>
|
|
</div>
|
|
</span>
|
|
),
|
|
{
|
|
id: "global-confirm-toast", // 중복 토스트 방지
|
|
duration: Infinity, // 자동 사라짐 방지
|
|
}
|
|
);
|
|
};
|
|
|
|
export const showConfirmToast = (message: string) => {
|
|
toast.custom(
|
|
(t) => (
|
|
<div
|
|
className={`min-h-[140px] w-[500px] rounded-xl border border-border bg-card p-6 text-card-foreground shadow-lg
|
|
${t.visible ? "animate-enter" : "animate-leave"}`}
|
|
>
|
|
<div className="rounded-md bg-accent px-4 py-2 text-3xl font-extrabold text-destructive shadow-md">
|
|
{message}
|
|
</div>{" "}
|
|
<div className="flex justify-end gap-2">
|
|
<Button
|
|
size={"sm"}
|
|
className="rounded bg-secondary px-4 py-1.5 text-sm text-secondary-foreground hover:bg-accent"
|
|
onClick={() => toast.dismiss(t.id)}
|
|
>
|
|
확인완료
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
),
|
|
{ duration: Infinity }
|
|
); // 수동 닫기
|
|
};
|
|
|
|
// yyyy-mm-dd 형태로 포맷
|
|
export const formatYyyymmdd = (val: string | undefined) => {
|
|
if (!val) return "";
|
|
if (/^\d{8}$/.test(val)) {
|
|
return moment(val, "YYYYMMDD").format("YYYY-MM-DD");
|
|
}
|
|
const m = moment(val);
|
|
return m.isValid() ? m.format("YYYY-MM-DD") : val;
|
|
};
|
|
|
|
/**
|
|
* 문자열 숫자(콤마 포함 가능)를 반올림 처리해 반환.
|
|
* 숫자가 아닌 경우(예: "확인불가")는 원래 값을 그대로 반환.
|
|
*
|
|
* @param value 입력 문자열
|
|
* @param digits 소수점 자릿수 (기본 0 = 정수 반올림)
|
|
*/
|
|
export const formatArchArea = (value: string, digits: number = 0) => {
|
|
// "확인불가" 등 숫자 아님 → 그대로 반환
|
|
const cleaned = value.replace(/,/g, "");
|
|
const num = parseFloat(cleaned);
|
|
|
|
if (isNaN(num)) {
|
|
return value;
|
|
}
|
|
|
|
// 반올림 (digits 자릿수까지)
|
|
const rounded = num.toFixed(digits);
|
|
|
|
// 다시 천 단위 콤마 찍어서 반환
|
|
return Number(rounded).toLocaleString();
|
|
};
|
|
|
|
export const codeToText = (val: string, codeName: string) => {
|
|
if (val === "" || val === undefined) {
|
|
return "";
|
|
}
|
|
|
|
return lwCds[codeName]?.find((cd) => cd.val === val)?.text;
|
|
};
|
|
|
|
export const textToCode = (text: string, codeName: string) => {
|
|
if (text === "" || text === undefined) {
|
|
return "";
|
|
}
|
|
|
|
return lwCds[codeName]?.find((cd) => cd.text === text)?.val;
|
|
};
|
|
|
|
// export const handleLaeSeCdChange = (
|
|
// laeSeCd: string,
|
|
// cityCd: SelectDataType[],
|
|
// sggCd: SelectDataType[]
|
|
// ): { isCityCd: SelectDataType[]; isSggCd: SelectDataType[] } => {
|
|
// if (laeSeCd.length !== 5 && cityCd.length > 0) {
|
|
// const city =
|
|
// laeSeCd === "0" ? cityCd : cityCd.filter((item) => item.val === laeSeCd);
|
|
|
|
// const sgg = sggCd.filter((item) => item.val.slice(0, 2) === laeSeCd);
|
|
// sgg.unshift({ text: "전체", val: "0" });
|
|
|
|
// return { isCityCd: city, isSggCd: sgg };
|
|
// } else if (laeSeCd.length === 5 && cityCd.length > 0 && sggCd.length > 0) {
|
|
// const city = cityCd.filter((item) => item.val === laeSeCd.slice(0, 2));
|
|
|
|
// const sgg = sggCd.filter((item) => item.val === laeSeCd);
|
|
// sgg.unshift({ text: "전체", val: "0" });
|
|
|
|
// return { isCityCd: city, isSggCd: sgg };
|
|
// }
|
|
|
|
// // 기본 반환값 (조건에 안 맞을 때)
|
|
// return { isCityCd: [], isSggCd: [] };
|
|
// };
|
|
|
|
|
|
|
|
|
|
export function handleLaeSeCdChange(
|
|
laeSeCd: string,
|
|
cityCd: SelectDataType[] = [],
|
|
sggCd: SelectDataType[] = []
|
|
): { isCityCd: SelectDataType[]; isSggCd: SelectDataType[] } {
|
|
const isAll = laeSeCd === "0"; // 전체
|
|
const isSgg = laeSeCd.length === 5; // 시군구 코드
|
|
const prefix = isAll ? "" : laeSeCd.slice(0, 2); // 시 코드(앞 2자리)
|
|
|
|
// 시 리스트: 전체면 전부, SGG 선택이면 해당 시만, 시 선택이면 그 시만
|
|
const isCityCd = isAll
|
|
? cityCd
|
|
: isSgg
|
|
? cityCd.filter(city => city.val === prefix)
|
|
: cityCd.filter(city => city.val === laeSeCd);
|
|
|
|
// 시군구 리스트: 전체면 비움(+ 전체만), 아니면 prefix로 필터
|
|
const sggInCity = isAll
|
|
? []
|
|
: sggCd.filter(s => s.val.slice(0, 2) === prefix);
|
|
|
|
// 선택된 SGG가 있으면 맨 위로(옵션)
|
|
const sggOrdered = isSgg
|
|
? [...sggInCity].sort((a, b) =>
|
|
a.val === laeSeCd ? -1 : b.val === laeSeCd ? 1 : 0
|
|
)
|
|
: sggInCity;
|
|
|
|
return {
|
|
isCityCd,
|
|
isSggCd: [{ text: "전체", val: "0" }, ...sggOrdered],
|
|
};
|
|
}
|