578 lines
18 KiB
TypeScript
578 lines
18 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useMemo, useRef, useState, type ReactNode, type RefObject } from "react";
|
|
import SubPageHeader from "@/components/SubPageHeader";
|
|
import PostcodeModal from "@/components/PostcodeModal";
|
|
import type { AddrItem } from "@/app/store/sdmMng/postcode-service";
|
|
import { useDmStore } from "@/app/store";
|
|
import { fetchType, getFetch, postFetch, putFetch } from "@/components/ui/api";
|
|
import { getCookie } from "@common/components/lib";
|
|
|
|
type Option = { value: string; label: string };
|
|
|
|
type SessionUser = {
|
|
dmdInfoId?: string;
|
|
dmdCoNm?: string;
|
|
zip?: string;
|
|
daddr?: string;
|
|
telno?: string;
|
|
ctpvCd?: string;
|
|
sggCd?: string;
|
|
rgtr?: string;
|
|
[key: string]: unknown;
|
|
};
|
|
|
|
const TOKEN_ID_COOKIE = "token-id";
|
|
const FALLBACK_UUID = "c475824c-0e82-4a06-a282-2b55a20df265";
|
|
const normalizeRegionName = (value: string) => value.replace(/\s+/g, "").trim();
|
|
const isSameRegionName = (left: string, right: string) => {
|
|
const a = normalizeRegionName(left);
|
|
const b = normalizeRegionName(right);
|
|
return Boolean(a && b && (a === b || a.includes(b) || b.includes(a)));
|
|
};
|
|
const formatKoreanPhoneNumber = (value: string) => {
|
|
const digits = value.replace(/\D/g, "").slice(0, 11); // 숫자만 추출하고 최대 11자리까지만 사용
|
|
if (!digits) return ""; // 입력이 비어 있으면 빈 문자열 반환
|
|
|
|
if (/^1\d{0,7}$/.test(digits)) { // 15xx/16xx/18xx 같은 대표번호 패턴
|
|
if (digits.length <= 4) return digits; // 아직 뒷자리가 부족하면 원문 유지
|
|
return `${digits.slice(0, 4)}-${digits.slice(4, 8)}`; // xxxx-xxxx
|
|
}
|
|
|
|
if (digits.startsWith("02")) { // 서울 지역번호(02)
|
|
const rest = digits.slice(2); // 국번/가입자번호 구간
|
|
if (!rest) return "02"; // 02만 입력된 상태
|
|
if (rest.length <= 3) return `02-${rest}`; // 02-xxx
|
|
if (rest.length <= 7) return `02-${rest.slice(0, 3)}-${rest.slice(3)}`; // 02-xxx-xxxx
|
|
return `02-${rest.slice(0, 4)}-${rest.slice(4, 8)}`; // 02-xxxx-xxxx
|
|
}
|
|
|
|
if (digits.startsWith("0")) { // 02 제외 일반 지역번호/휴대폰(010 포함)
|
|
const area = digits.slice(0, 3); // 앞 3자리 식별자
|
|
const rest = digits.slice(3); // 나머지 번호
|
|
if (!rest) return area; // 식별자만 입력된 상태
|
|
if (rest.length <= 3) return `${area}-${rest}`; // 0xx-xxx
|
|
if (rest.length <= 7) return `${area}-${rest.slice(0, 3)}-${rest.slice(3)}`; // 0xx-xxx-xxxx
|
|
return `${area}-${rest.slice(0, 4)}-${rest.slice(4, 8)}`; // 0xx-xxxx-xxxx
|
|
}
|
|
|
|
if (digits.length <= 4) return digits; // 예외 입력: 앞 4자리까지
|
|
if (digits.length <= 8) return `${digits.slice(0, 4)}-${digits.slice(4)}`; // xxxx-xxxx
|
|
return `${digits.slice(0, 4)}-${digits.slice(4, 8)}-${digits.slice(8, 11)}`; // xxxx-xxxx-xxx
|
|
};
|
|
|
|
function Divider() {
|
|
return <div className="h-px w-full bg-neutral-200" />;
|
|
}
|
|
|
|
function FormRow({
|
|
label,
|
|
required,
|
|
children,
|
|
}: {
|
|
label: ReactNode;
|
|
required?: boolean;
|
|
children: ReactNode;
|
|
}) {
|
|
return (
|
|
<div className="grid grid-cols-12">
|
|
<div className="col-span-12 lg:col-span-2 flex items-center justify-center text-center bg-emerald-50/70 px-6 py-6">
|
|
<div className="flex items-center gap-1 text-lg font-semibold text-neutral-800">
|
|
<span>{label}</span>
|
|
{required ? <span className="text-red-500">*</span> : null}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="col-span-12 lg:col-span-10 px-6 py-6">{children}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function TextInput({
|
|
value,
|
|
onChange,
|
|
placeholder,
|
|
type = "text",
|
|
disabled,
|
|
inputRef,
|
|
className = "",
|
|
}: {
|
|
value: string;
|
|
onChange: (v: string) => void;
|
|
placeholder?: string;
|
|
type?: string;
|
|
disabled?: boolean;
|
|
inputRef?: RefObject<HTMLInputElement | null>;
|
|
className?: string;
|
|
}) {
|
|
return (
|
|
<input
|
|
ref={inputRef}
|
|
type={type}
|
|
value={value}
|
|
disabled={disabled}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
placeholder={placeholder}
|
|
className={[
|
|
"h-11 w-full rounded border border-neutral-300 bg-white px-3 text-sm text-neutral-800",
|
|
"placeholder:text-neutral-400 focus:border-neutral-400 focus:outline-none",
|
|
disabled ? "bg-neutral-100 text-neutral-400" : "",
|
|
className,
|
|
].join(" ")}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function SelectBox({
|
|
value,
|
|
onChange,
|
|
options,
|
|
className = "",
|
|
}: {
|
|
value: string;
|
|
onChange: (v: string) => void;
|
|
options: Option[];
|
|
className?: string;
|
|
}) {
|
|
return (
|
|
<select
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
className={[
|
|
"h-11 w-[220px] rounded border border-neutral-300 bg-white px-3 text-sm text-neutral-800",
|
|
"focus:border-neutral-400 focus:outline-none",
|
|
className,
|
|
].join(" ")}
|
|
>
|
|
{options.map((o) => (
|
|
<option key={o.value} value={o.value}>
|
|
{o.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
);
|
|
}
|
|
|
|
export default function DemandPlaceRegisterPage() {
|
|
const { Common, initializeCommonData } = useDmStore((state) => state);
|
|
const { selectSidoData, selectSigunData } = Common;
|
|
|
|
const [editMode, setEditMode] = useState(false);
|
|
const [sessionUser, setSessionUser] = useState<SessionUser | null>(null);
|
|
|
|
const [sido, setSido] = useState("");
|
|
const [sigungu, setSigungu] = useState("");
|
|
|
|
const [company, setCompany] = useState("");
|
|
const [bizNo, setBizNo] = useState("");
|
|
const [workerNo, setWorkerNo] = useState("");
|
|
const [noWorkerNo, setNoWorkerNo] = useState(false);
|
|
|
|
const [zip, setZip] = useState("");
|
|
const [addr1, setAddr1] = useState("");
|
|
const [addr2, setAddr2] = useState("");
|
|
const [isPostcodeModalOpen, setIsPostcodeModalOpen] = useState(false);
|
|
const [focusDetailAddrOnClose, setFocusDetailAddrOnClose] = useState(false);
|
|
const detailAddrRef = useRef<HTMLInputElement | null>(null);
|
|
|
|
const [tel, setTel] = useState("");
|
|
const [pw, setPw] = useState("");
|
|
const [pw2, setPw2] = useState("");
|
|
|
|
useEffect(() => {
|
|
initializeCommonData();
|
|
}, [initializeCommonData]);
|
|
|
|
useEffect(() => {
|
|
if (typeof window === "undefined") return;
|
|
|
|
const rawUser = sessionStorage.getItem("user");
|
|
if (!rawUser) {
|
|
setEditMode(false);
|
|
setSessionUser(null);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const user = JSON.parse(rawUser) as SessionUser;
|
|
setSessionUser(user);
|
|
setEditMode(true);
|
|
|
|
const fullId = String(user.dmdInfoId ?? "").trim();
|
|
const bizDigits = fullId.slice(0, 10);
|
|
const workerDigits = fullId.slice(10, 14);
|
|
|
|
setBizNo(bizDigits);
|
|
if (workerDigits && workerDigits !== "0000") {
|
|
setNoWorkerNo(false);
|
|
setWorkerNo(workerDigits);
|
|
} else {
|
|
setNoWorkerNo(true);
|
|
setWorkerNo("");
|
|
}
|
|
|
|
setCompany(String(user.dmdCoNm ?? ""));
|
|
setZip(String(user.zip ?? ""));
|
|
setAddr1(String(user.daddr ?? ""));
|
|
setAddr2("");
|
|
setTel(formatKoreanPhoneNumber(String(user.telno ?? "")));
|
|
setSido(String(user.ctpvCd ?? ""));
|
|
setSigungu(String(user.sggCd ?? ""));
|
|
} catch {
|
|
setEditMode(false);
|
|
setSessionUser(null);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!focusDetailAddrOnClose || isPostcodeModalOpen) return;
|
|
|
|
if (!addr1.trim()) {
|
|
setFocusDetailAddrOnClose(false);
|
|
return;
|
|
}
|
|
|
|
const timer = setTimeout(() => {
|
|
detailAddrRef.current?.focus();
|
|
setFocusDetailAddrOnClose(false);
|
|
}, 0);
|
|
|
|
return () => clearTimeout(timer);
|
|
}, [addr1, focusDetailAddrOnClose, isPostcodeModalOpen]);
|
|
|
|
const sidoOptions = useMemo<Option[]>(() => {
|
|
const mapped = selectSidoData
|
|
.filter((item) => item.val !== "0")
|
|
.map((item) => ({ value: item.val, label: item.text }));
|
|
|
|
return [{ value: "", label: "선택" }, ...mapped];
|
|
}, [selectSidoData]);
|
|
|
|
const sigunguOptions = useMemo<Option[]>(() => {
|
|
if (!sido) return [{ value: "", label: "선택" }];
|
|
|
|
const mapped = selectSigunData
|
|
.filter((item) => item.val.substring(0, 2) === sido)
|
|
.map((item) => ({ value: item.val, label: item.text }));
|
|
|
|
return [{ value: "", label: "선택" }, ...mapped];
|
|
}, [sido, selectSigunData]);
|
|
|
|
const formatBusinessNumber = (value: string) => {
|
|
const digitsOnly = value.replace(/\D/g, "").slice(0, 10);
|
|
if (digitsOnly.length <= 3) return digitsOnly;
|
|
if (digitsOnly.length <= 5) {
|
|
return `${digitsOnly.slice(0, 3)}-${digitsOnly.slice(3)}`;
|
|
}
|
|
return `${digitsOnly.slice(0, 3)}-${digitsOnly.slice(3, 5)}-${digitsOnly.slice(5)}`;
|
|
};
|
|
|
|
const checkDuplicate = async (fullDmdInfoId: string) => {
|
|
try {
|
|
const res = await getFetch(fetchType.WR, `/reuseFc/request/${fullDmdInfoId}`);
|
|
const responseData = (res?.data ?? res) as { dmdInfoId?: string } | undefined;
|
|
return Boolean(responseData?.dmdInfoId);
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const onSubmit = async () => {
|
|
const bizDigits = bizNo.replace(/\D/g, "").slice(0, 10);
|
|
const workerDigits = workerNo.replace(/\D/g, "").slice(0, 4);
|
|
const telDigits = tel.replace(/\D/g, "");
|
|
const password = pw.trim();
|
|
const passwordConfirm = pw2.trim();
|
|
|
|
if (!sido || sido === "0") return alert("시도/시군구를 선택해 주세요.");
|
|
if (!sigungu || sigungu === "0") return alert("시도/시군구를 선택해 주세요.");
|
|
if (!company.trim()) return alert("회사명을 입력해 주세요.");
|
|
if (company.trim().length > 20) return alert("회사명은 20자 이내로 입력해주세요.");
|
|
|
|
if (!bizDigits) return alert("사업자등록번호를 입력해 주세요.");
|
|
if (!/^\d{10}$/.test(bizDigits)) {
|
|
return alert("사업자등록번호는 10자리 숫자만 입력 가능합니다.");
|
|
}
|
|
|
|
if (!noWorkerNo) {
|
|
if (!workerDigits) return alert("종사업자등록번호를 입력해 주세요.");
|
|
if (!/^\d{4}$/.test(workerDigits)) {
|
|
return alert("종사업자등록번호는 4자리 숫자만 입력 가능합니다.");
|
|
}
|
|
}
|
|
|
|
const mergedAddress = [addr1.trim(), addr2.trim()].filter(Boolean).join(" ");
|
|
if (!mergedAddress) return alert("사업자주소를 입력해 주세요.");
|
|
if (mergedAddress.length > 40) return alert("주소는 40자 이내로 입력해주세요.");
|
|
|
|
if (!telDigits) return alert("전화번호를 입력해 주세요.");
|
|
if (!/^\d{8,11}$/.test(telDigits)) {
|
|
return alert("전화번호는 8~11자리 숫자만 입력 가능합니다.");
|
|
}
|
|
|
|
if (!editMode) {
|
|
if (!password) return alert("비밀번호를 입력해 주세요.");
|
|
if (password.length < 8 || password.length > 16) {
|
|
return alert("비밀번호는 8자 이상 16자 이내로 입력해주세요.");
|
|
}
|
|
if (!passwordConfirm) return alert("비밀번호 확인을 입력해 주세요.");
|
|
if (password !== passwordConfirm) return alert("비밀번호가 일치하지 않습니다.");
|
|
} else if ((password || passwordConfirm) && password !== passwordConfirm) {
|
|
return alert("비밀번호가 일치하지 않습니다.");
|
|
}
|
|
|
|
const fullDmdInfoId = bizDigits + (noWorkerNo ? "0000" : workerDigits);
|
|
|
|
if (!editMode) {
|
|
const isDuplicate = await checkDuplicate(fullDmdInfoId);
|
|
if (isDuplicate) return alert("이미 등록된 사업자등록번호입니다.");
|
|
}
|
|
|
|
const myUUID = getCookie(TOKEN_ID_COOKIE) || FALLBACK_UUID;
|
|
|
|
const payload: Record<string, unknown> = {
|
|
ctpvCd: sido,
|
|
sggCd: sigungu,
|
|
dmdCoNm: company.trim(),
|
|
dmdInfoId: fullDmdInfoId,
|
|
bzmnNo: bizDigits,
|
|
zip: zip.trim(),
|
|
daddr: mergedAddress,
|
|
telno: telDigits,
|
|
rgtr: (sessionUser?.rgtr as string) || myUUID,
|
|
};
|
|
|
|
if (password) {
|
|
payload.dmdAplyPswd = password;
|
|
}
|
|
|
|
try {
|
|
if (editMode) {
|
|
payload.mdfr = myUUID;
|
|
await putFetch("/reuseFc/request", payload);
|
|
|
|
if (typeof window !== "undefined") {
|
|
const nextSessionUser = {
|
|
...(sessionUser ?? {}),
|
|
...payload,
|
|
};
|
|
delete nextSessionUser.dmdAplyPswd;
|
|
sessionStorage.setItem("user", JSON.stringify(nextSessionUser));
|
|
}
|
|
|
|
alert("수요처 수정 완료!");
|
|
} else {
|
|
await postFetch("/reuseFc/request", payload);
|
|
alert("수요처 등록 완료!");
|
|
}
|
|
|
|
setPw("");
|
|
setPw2("");
|
|
} catch (error) {
|
|
console.error("Failed to submit demand place", error);
|
|
alert(editMode ? "수요처 수정에 실패하였습니다." : "수요처 등록에 실패하였습니다.");
|
|
}
|
|
};
|
|
|
|
const handleSelectAddress = (item: AddrItem) => {
|
|
setZip(item.zipNo);
|
|
setAddr1(item.roadAddr);
|
|
setFocusDetailAddrOnClose(true);
|
|
const matchedSido = selectSidoData.find(
|
|
(region) =>
|
|
region.val !== "0" &&
|
|
isSameRegionName(region.text ?? "", item.ctpvNm ?? ""),
|
|
);
|
|
|
|
if (matchedSido) {
|
|
setSido(matchedSido.val);
|
|
const matchedSigungu = selectSigunData.find(
|
|
(region) =>
|
|
region.val !== "0" &&
|
|
region.val.startsWith(matchedSido.val) &&
|
|
isSameRegionName(region.text ?? "", item.sggNm ?? ""),
|
|
);
|
|
setSigungu(matchedSigungu?.val ?? "");
|
|
}
|
|
|
|
setIsPostcodeModalOpen(false);
|
|
};
|
|
|
|
const pageTitle = editMode ? "수요처수정" : "수요처등록";
|
|
|
|
return (
|
|
<main className="bg-white">
|
|
<SubPageHeader
|
|
title={pageTitle}
|
|
crumbs={[
|
|
{ label: "Home", href: "/reuse" },
|
|
{ label: "수요신청", href: "/reuse/apply" },
|
|
{ label: pageTitle },
|
|
]}
|
|
/>
|
|
|
|
<div className="border border-neutral-200 bg-white">
|
|
<FormRow label="시도" required>
|
|
<SelectBox
|
|
value={sido}
|
|
onChange={(v) => {
|
|
setSido(v);
|
|
setSigungu("");
|
|
}}
|
|
options={sidoOptions}
|
|
/>
|
|
</FormRow>
|
|
|
|
<Divider />
|
|
|
|
<FormRow label="시군구" required>
|
|
<select
|
|
value={sigungu}
|
|
onChange={(e) => setSigungu(e.target.value)}
|
|
className="h-11 w-[220px] rounded border border-neutral-300 bg-white px-3 text-sm text-neutral-800 focus:border-neutral-400 focus:outline-none"
|
|
>
|
|
{sigunguOptions.map((o) => (
|
|
<option key={o.value} value={o.value}>
|
|
{o.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</FormRow>
|
|
|
|
<Divider />
|
|
|
|
<FormRow label="회사명" required>
|
|
<div className="max-w-[420px]">
|
|
<TextInput
|
|
value={company}
|
|
onChange={setCompany}
|
|
placeholder="회사명을 입력해주세요."
|
|
/>
|
|
</div>
|
|
</FormRow>
|
|
|
|
<Divider />
|
|
|
|
<FormRow label="사업자등록번호" required>
|
|
<div className="max-w-[420px]">
|
|
<TextInput
|
|
value={formatBusinessNumber(bizNo)}
|
|
onChange={(v) => setBizNo(v.replace(/\D/g, "").slice(0, 10))}
|
|
placeholder="사업자 등록번호를 입력해주세요."
|
|
/>
|
|
</div>
|
|
</FormRow>
|
|
|
|
<Divider />
|
|
|
|
<FormRow label="종사자등록번호">
|
|
<div className="flex flex-wrap items-center gap-4">
|
|
<div className="w-[420px] max-w-full">
|
|
<TextInput
|
|
value={workerNo}
|
|
onChange={(v) => setWorkerNo(v.replace(/\D/g, "").slice(0, 4))}
|
|
placeholder="종사자등록번호를 입력해주세요."
|
|
disabled={noWorkerNo}
|
|
/>
|
|
</div>
|
|
|
|
<label className="flex items-center gap-2 text-sm text-neutral-700">
|
|
<input
|
|
type="checkbox"
|
|
checked={noWorkerNo}
|
|
onChange={(e) => {
|
|
setNoWorkerNo(e.target.checked);
|
|
if (e.target.checked) setWorkerNo("");
|
|
}}
|
|
className="h-4 w-4 rounded border-neutral-300"
|
|
/>
|
|
해당없음
|
|
</label>
|
|
</div>
|
|
</FormRow>
|
|
|
|
<Divider />
|
|
|
|
<FormRow label="사업자주소" required>
|
|
<div className="max-w-[420px] space-y-2">
|
|
<div className="flex gap-2">
|
|
<div className="w-[180px]">
|
|
<TextInput value={zip} onChange={setZip} placeholder="우편번호" />
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsPostcodeModalOpen(true)}
|
|
className="h-11 rounded bg-[#888] px-4 text-base text-white hover:bg-neutral-700"
|
|
>
|
|
우편번호 검색
|
|
</button>
|
|
</div>
|
|
|
|
<TextInput value={addr1} onChange={setAddr1} placeholder="주소" />
|
|
<TextInput
|
|
value={addr2}
|
|
onChange={setAddr2}
|
|
placeholder="상세 주소를 입력하세요."
|
|
inputRef={detailAddrRef}
|
|
/>
|
|
</div>
|
|
</FormRow>
|
|
|
|
<Divider />
|
|
|
|
<FormRow label="전화번호" required>
|
|
<div className="max-w-[420px]">
|
|
<TextInput
|
|
value={tel}
|
|
onChange={(v) => setTel(formatKoreanPhoneNumber(v))}
|
|
placeholder="전화번호를 입력하세요."
|
|
/>
|
|
</div>
|
|
</FormRow>
|
|
|
|
<Divider />
|
|
|
|
<FormRow label="비밀번호" required={!editMode}>
|
|
<div className="max-w-[420px]">
|
|
<TextInput
|
|
value={pw}
|
|
onChange={setPw}
|
|
placeholder="비밀번호를 입력하세요."
|
|
type="password"
|
|
/>
|
|
</div>
|
|
</FormRow>
|
|
|
|
<Divider />
|
|
|
|
<FormRow label="비밀번호 확인" required={!editMode}>
|
|
<div className="max-w-[420px]">
|
|
<TextInput
|
|
value={pw2}
|
|
onChange={setPw2}
|
|
placeholder="비밀번호를 한번 더 입력하세요."
|
|
type="password"
|
|
/>
|
|
</div>
|
|
</FormRow>
|
|
</div>
|
|
|
|
<div className="flex justify-center py-12">
|
|
<button
|
|
type="button"
|
|
onClick={onSubmit}
|
|
className="h-11 rounded-md bg-[#0B4EA2] px-10 text-lg text-white hover:opacity-90"
|
|
>
|
|
{editMode ? "수요처 수정" : "수요처 등록"}
|
|
</button>
|
|
</div>
|
|
|
|
<PostcodeModal
|
|
open={isPostcodeModalOpen}
|
|
onClose={() => setIsPostcodeModalOpen(false)}
|
|
onSelect={handleSelectAddress}
|
|
/>
|
|
</main>
|
|
);
|
|
}
|