[as frontend] 입력화면 콤포넌트화 처리
This commit is contained in:
@@ -69,22 +69,9 @@ const num = (v: string) => parseFloat(v.replace(/,/g, "") || "0");
|
||||
const fmt = (v: number) => (isNaN(v) ? "" : v.toLocaleString());
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 공통 셀 컴포넌트 (컴포넌트 외부에 선언해야 re-mount 방지) */
|
||||
/* 공통 셀 컴포넌트 분리: widgets/cnfm/ui/cells.tsx에서 import */
|
||||
/* ------------------------------------------------------------------ */
|
||||
const NumCell = ({ value, onChange }: { value: string; onChange: (v: string) => void }) => (
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className="cnfm-input w-full min-w-0 text-right"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
|
||||
const ReadCell = ({ value }: { value: string | number }) => (
|
||||
<td className="bg-gray-50 pr-2 text-right text-[1.25rem] font-semibold">{value}</td>
|
||||
);
|
||||
import { NumCell, ReadCell, PrivacyConsent, EtcAttachments } from '@/widgets/cnfm';
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 메인 컴포넌트 */
|
||||
@@ -112,7 +99,6 @@ function CnfmRegistPage() {
|
||||
sessionStorage.removeItem(INSTL_SNCN_REGISTER_SELECTION_KEY);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/* ── 기본 정보 ── */
|
||||
const [bzNm, setBzNm] = useState("");
|
||||
const [bzBgngYr, setBzBgngYr] = useState("");
|
||||
@@ -129,23 +115,6 @@ function CnfmRegistPage() {
|
||||
/* ── 개인정보 동의 ── */
|
||||
const [privacyAgree, setPrivacyAgree] = useState<"Y" | "N">("N");
|
||||
|
||||
/* ── 기타 첨부파일 ── */
|
||||
type EtcFile = {
|
||||
id: string;
|
||||
name: string;
|
||||
ext: string;
|
||||
size: string;
|
||||
uploader: string;
|
||||
atflCd: string;
|
||||
atflId: string;
|
||||
};
|
||||
const [etcFiles, setEtcFiles] = useState<EtcFile[]>([]);
|
||||
const [etcNoticeOpen, setEtcNoticeOpen] = useState(true);
|
||||
const [etcFileGroupId, setEtcFileGroupId] = useState("");
|
||||
const [etcUploading, setEtcUploading] = useState(false);
|
||||
const etcFileGroupIdRef = useRef("");
|
||||
|
||||
/* ── 필수 첨부파일 ── */
|
||||
const { data: requiredAttachmentDefs = [] } = useQuery<EsntlAtchFileMngTreeResponseDto[]>({
|
||||
queryKey: ["esntlAtchFileTree", docSeComCd],
|
||||
queryFn: () => esntlAtchFileMngService.findTree({ docSeComCd }),
|
||||
@@ -237,8 +206,24 @@ function CnfmRegistPage() {
|
||||
upload.start();
|
||||
};
|
||||
|
||||
/** 기타첨부파일 삭제 핸들러: 백엔드에 DELETE 요청을 보내 소프트삭제 처리 후 상태 갱신 */
|
||||
const handleDeleteEtcFile = async (fileId: string, atflId: string) => {
|
||||
/* ── 기타 첨부파일 ── */
|
||||
type EtcFile = {
|
||||
id: string;
|
||||
name: string;
|
||||
ext: string;
|
||||
size: string;
|
||||
uploader: string;
|
||||
atflCd: string;
|
||||
atflId: string;
|
||||
};
|
||||
const [etcFiles, setEtcFiles] = useState<EtcFile[]>([]);
|
||||
const [etcNoticeOpen, setEtcNoticeOpen] = useState(true);
|
||||
const [etcFileGroupId, setEtcFileGroupId] = useState("");
|
||||
const [etcUploading, setEtcUploading] = useState(false);
|
||||
const etcFileGroupIdRef = useRef("");
|
||||
|
||||
/** 기타첨부파일 삭제 핸들러: 백엔드에 DELETE 요청을 보내 소프트삭제 처리 후 상태 갱신 */
|
||||
const handleDeleteEtcFile = async (fileId: string, atflId: string) => {
|
||||
try {
|
||||
if (atflId) {
|
||||
await deleteAttachment(atflId);
|
||||
@@ -932,61 +917,35 @@ function CnfmRegistPage() {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* ── 개인정보 동의 ── */}
|
||||
<div className="cnfm-card p-4">
|
||||
<h3 className="mb-3 border-b pb-2 text-[1.5rem] font-bold text-gray-700">개인정보 동의</h3>
|
||||
<div className="mb-3 rounded border bg-gray-50 p-3 text-[1.25rem] leading-relaxed text-gray-600 max-h-80 overflow-auto whitespace-pre-wrap pr-2">
|
||||
<p className="mb-1 font-semibold">[ 개인정보 수집항목 ]</p>
|
||||
<div>
|
||||
* 필수항목 : 소속기관, 소속부서, 담당자, 직급, 사무실 번호, FAX 번호<br />
|
||||
․ 선택항목 : 휴대폰 번호, 이메일 주소<br />
|
||||
․ 기타항목 : IP Address, 쿠키, 시스템 접속 및 이용 기록<br />
|
||||
※ 시스템 이용과정에서 기타 항목의 정보들이 자동으로 생성되어 수집될 수 있습니다.
|
||||
</div>
|
||||
|
||||
<p className="mt-4 font-semibold">[ 개인정보 이용목적 ]</p>
|
||||
<div>
|
||||
․ 국가하수도정보시스템 웹서비스 이용에 따른 본인 확인 절차에 이용<br />
|
||||
․ 국가하수도정보시스템 사용에 필요한 권한 부여<br />
|
||||
․ 사용자별 맞춤 컨텐츠 (처리시설의 운영 데이터 조회, 수질현황 데이터 조회, 예산 편성 / 예산편성 등) 제공에 이용
|
||||
</div>
|
||||
|
||||
<div className="tit mt-4">● 개인정보의 보유 및 이용기간</div>
|
||||
<div>
|
||||
․ 수집된 고객의 개인정보는 2년 동안 보유하며 그 이후에는 문서를 파쇄합니다.<br />
|
||||
․ 정보제공자가 개인정보 파쇄를 요청할 경우 즉시 파쇄합니다.
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<div className="tit">● 개인정보 제공 거부에 따른 제한사항</div>
|
||||
<div>
|
||||
․ 귀하는 개인정보 제공 동의를 거부할 권리가 있으나, 동의 거부 시 국가하수도정보시스템 이용 서비스에 제한이 있을 수 있습니다.
|
||||
</div>
|
||||
{/* ── 개인정보 동의 (위젯으로 분리) ── */}
|
||||
<PrivacyConsent value={privacyAgree} onChange={setPrivacyAgree}>
|
||||
<p className="mb-1 font-semibold">[ 개인정보 수집항목 ]</p>
|
||||
<div>
|
||||
* 필수항목 : 소속기관, 소속부서, 담당자, 직급, 사무실 번호, FAX 번호<br />
|
||||
․ 선택항목 : 휴대폰 번호, 이메일 주소<br />
|
||||
․ 기타항목 : IP Address, 쿠키, 시스템 접속 및 이용 기록<br />
|
||||
※ 시스템 이용과정에서 기타 항목의 정보들이 자동으로 생성되어 수집될 수 있습니다.
|
||||
</div>
|
||||
|
||||
<div className="flex gap-6 text-[1.3rem]">
|
||||
<label className="flex cursor-pointer items-center gap-2 font-semibold">
|
||||
<input
|
||||
type="radio"
|
||||
name="privacyAgree"
|
||||
value="Y"
|
||||
checked={privacyAgree === "Y"}
|
||||
onChange={() => setPrivacyAgree("Y")}
|
||||
/>
|
||||
예 (동의)
|
||||
</label>
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="privacyAgree"
|
||||
value="N"
|
||||
checked={privacyAgree === "N"}
|
||||
onChange={() => setPrivacyAgree("N")}
|
||||
/>
|
||||
아니오 (미동의)
|
||||
</label>
|
||||
<p className="mt-4 font-semibold">[ 개인정보 이용목적 ]</p>
|
||||
<div>
|
||||
․ 국가하수도정보시스템 웹서비스 이용에 따른 본인 확인 절차에 이용<br />
|
||||
․ 국가하수도정보시스템 사용에 필요한 권한 부여<br />
|
||||
․ 사용자별 맞춤 컨텐츠 (처리시설의 운영 데이터 조회, 수질현황 데이터 조회, 예산 편성 / 예산편성 등) 제공에 이용
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="tit mt-4">● 개인정보의 보유 및 이용기간</div>
|
||||
<div>
|
||||
․ 수집된 고객의 개인정보는 2년 동안 보유하며 그 이후에는 문서를 파쇄합니다.<br />
|
||||
․ 정보제공자가 개인정보 파쇄를 요청할 경우 즉시 파쇄합니다.
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<div className="tit">● 개인정보 제공 거부에 따른 제한사항</div>
|
||||
<div>
|
||||
․ 귀하는 개인정보 제공 동의를 거부할 권리가 있으나, 동의 거부 시 국가하수도정보시스템 이용 서비스에 제한이 있을 수 있습니다.
|
||||
</div>
|
||||
</PrivacyConsent>
|
||||
|
||||
{/* ── 사업규모 ── */}
|
||||
<div className="cnfm-card p-4">
|
||||
@@ -1345,74 +1304,12 @@ function CnfmRegistPage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 파일 업로드 영역 */}
|
||||
<div className="mb-3 flex flex-nowrap items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
className="cnfm-input min-w-0 flex-1"
|
||||
placeholder="첨부문서를 등록해주세요"
|
||||
value={
|
||||
etcUploading
|
||||
? "업로드 중..."
|
||||
: etcFiles.length > 0
|
||||
? `${etcFiles.length}개 파일 등록됨`
|
||||
: ""
|
||||
}
|
||||
readOnly
|
||||
/>
|
||||
<label className="cnfm-btn-search shrink-0 cursor-pointer whitespace-nowrap text-[1.2rem]">
|
||||
{etcUploading ? "업로드중" : "첨부파일"}
|
||||
<input
|
||||
type="file"
|
||||
className="hidden"
|
||||
multiple
|
||||
onChange={handleEtcFilesChange}
|
||||
disabled={etcUploading}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* 첨부된 파일 목록 테이블 */}
|
||||
<div className="cnfm-table-wrap">
|
||||
<table className="cnfm-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>파일명</th>
|
||||
<th className="w-[8rem]">확장자</th>
|
||||
<th className="w-[8rem]">크기</th>
|
||||
<th className="w-[12rem]">등록자</th>
|
||||
<th className="w-[6rem]">삭제</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{etcFiles.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="cnfm-td-empty">
|
||||
첨부된 파일이 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
etcFiles.map((f) => (
|
||||
<tr key={f.id}>
|
||||
<td className="pl-2 text-left">{f.name}</td>
|
||||
<td>{f.ext}</td>
|
||||
<td>{f.size}</td>
|
||||
<td>{f.uploader}</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
className="text-[1.2rem] text-red-500 hover:text-red-700"
|
||||
onClick={() => void handleDeleteEtcFile(f.id, f.atflId)}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<EtcAttachments
|
||||
etcFiles={etcFiles}
|
||||
etcUploading={etcUploading}
|
||||
onFilesChange={handleEtcFilesChange}
|
||||
onDelete={(fileId, atflId) => void handleDeleteEtcFile(fileId, atflId)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ── 액션 버튼 ── */}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ui';
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from "react";
|
||||
|
||||
type EtcFile = {
|
||||
id: string;
|
||||
name: string;
|
||||
ext: string;
|
||||
size: string;
|
||||
uploader: string;
|
||||
atflCd: string;
|
||||
atflId: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
etcFiles: EtcFile[];
|
||||
etcUploading: boolean;
|
||||
onFilesChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onDelete: (fileId: string, atflId: string) => void;
|
||||
};
|
||||
|
||||
export function EtcAttachments({ etcFiles, etcUploading, onFilesChange, onDelete }: Props) {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-3 flex flex-nowrap items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
className="cnfm-input min-w-0 flex-1"
|
||||
placeholder="첨부문서를 등록해주세요"
|
||||
value={
|
||||
etcUploading ? "업로드 중..." : etcFiles.length > 0 ? `${etcFiles.length}개 파일 등록됨` : ""
|
||||
}
|
||||
readOnly
|
||||
/>
|
||||
<label className="cnfm-btn-search shrink-0 cursor-pointer whitespace-nowrap text-[1.2rem] flex items-center">
|
||||
{etcUploading ? "업로드중" : "첨부파일"}
|
||||
<input type="file" className="hidden" multiple onChange={onFilesChange} disabled={etcUploading} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="cnfm-table-wrap">
|
||||
<table className="cnfm-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>파일명</th>
|
||||
<th>확장자</th>
|
||||
<th>크기</th>
|
||||
<th>등록자</th>
|
||||
<th>액션</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{etcFiles.map((f) => (
|
||||
<tr key={f.id}>
|
||||
<td className="pr-2">{f.name}</td>
|
||||
<td className="text-center">{f.ext}</td>
|
||||
<td className="text-right">{f.size}</td>
|
||||
<td className="text-center">{f.uploader}</td>
|
||||
<td className="text-center">
|
||||
<button className="cnfm-btn-delete" onClick={() => onDelete(f.id, f.atflId)}>
|
||||
삭제
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EtcAttachments;
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
export const NumCell = ({ value, onChange }: { value: string; onChange: (v: string) => void }) => (
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className="cnfm-input w-full min-w-0 text-right"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
|
||||
export const ReadCell = ({ value }: { value: string | number }) => (
|
||||
<td className="bg-gray-50 pr-2 text-right text-[1.25rem] font-semibold">{value}</td>
|
||||
);
|
||||
|
||||
export default { NumCell, ReadCell };
|
||||
@@ -0,0 +1,3 @@
|
||||
export { NumCell, ReadCell } from './cells';
|
||||
export { PrivacyConsent } from './privacy-consent';
|
||||
export { EtcAttachments } from './attachments';
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
value: "Y" | "N";
|
||||
onChange: (v: "Y" | "N") => void;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function PrivacyConsent({ value, onChange, children }: Props) {
|
||||
return (
|
||||
<div className="cnfm-card p-4">
|
||||
<h3 className="mb-3 border-b pb-2 text-[1.5rem] font-bold text-gray-700">개인정보 동의</h3>
|
||||
<div className="mb-3 rounded border bg-gray-50 p-3 text-[1.25rem] leading-relaxed text-gray-600 max-h-80 overflow-auto whitespace-pre-wrap pr-2">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-6 text-[1.3rem]">
|
||||
<label className="flex cursor-pointer items-center gap-2 font-semibold">
|
||||
<input type="radio" name="privacyAgree" value="Y" checked={value === "Y"} onChange={() => onChange("Y")} />
|
||||
예 (동의)
|
||||
</label>
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input type="radio" name="privacyAgree" value="N" checked={value === "N"} onChange={() => onChange("N")} />
|
||||
아니오 (미동의)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PrivacyConsent;
|
||||
Reference in New Issue
Block a user