Merge branch 'iipbs/common/main' into iipbs/sn/dev
This commit is contained in:
@@ -35,6 +35,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"libphonenumber-js": "^1.12.9",
|
||||
"lucide-react": "^0.474.0",
|
||||
"path": "^0.12.7",
|
||||
"react-day-picker": "^8.10.1",
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
Button,
|
||||
} from "@common/components/ui";
|
||||
import { ko } from "date-fns/locale";
|
||||
import { format } from "date-fns";
|
||||
import { useState } from "react";
|
||||
import { MonthPicker } from "./mothPicker";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
|
||||
type MonthPickerProps = {
|
||||
field: { value: string; onChange: (date: string) => void }; // yyyyMM 문자열
|
||||
selectClass?: string;
|
||||
readOnly?: boolean;
|
||||
};
|
||||
|
||||
// yyyyMM → Date 객체
|
||||
const parseYYYYMM = (value: string) => {
|
||||
const year = Number(value.substring(0, 4));
|
||||
const month = Number(value.substring(4, 6)) - 1;
|
||||
return new Date(year, month);
|
||||
};
|
||||
|
||||
// Date 객체 → yyyyMM 문자열
|
||||
const formatDateToYYYYMM = (date: Date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
return `${year}${month}`;
|
||||
};
|
||||
|
||||
export const DateMonthPicker = ({
|
||||
field,
|
||||
selectClass,
|
||||
readOnly = false,
|
||||
}: MonthPickerProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const parsedDate = field?.value ? parseYYYYMM(field.value) : new Date();
|
||||
|
||||
const handleSelect = (
|
||||
date: Date | undefined,
|
||||
shouldClosePopover?: boolean
|
||||
) => {
|
||||
if (!readOnly && date) {
|
||||
field.onChange(formatDateToYYYYMM(date));
|
||||
if (shouldClosePopover) setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover open={open && !readOnly} onOpenChange={readOnly ? undefined : setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
color="white"
|
||||
disabled={readOnly}
|
||||
className={cn(
|
||||
selectClass ?? "w-[240px] justify-between pl-3 text-left font-normal",
|
||||
!field?.value && "text-muted-foreground",
|
||||
readOnly && "opacity-60 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
{parsedDate
|
||||
?format(parsedDate, "yyyy년 M월", { locale: ko })
|
||||
: "월을 선택하세요"}
|
||||
<CalendarIcon className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<MonthPicker
|
||||
currentMonth={parsedDate}
|
||||
onMonthChange={handleSelect}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
import { cn } from "../../lib/utils";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components/ui";
|
||||
import {
|
||||
eachMonthOfInterval,
|
||||
endOfYear,
|
||||
format,
|
||||
isEqual,
|
||||
isFuture,
|
||||
parse,
|
||||
startOfMonth,
|
||||
startOfToday,
|
||||
} from "date-fns";
|
||||
import { ko } from "date-fns/locale";
|
||||
|
||||
function getStartOfCurrentMonth() {
|
||||
return startOfMonth(startOfToday());
|
||||
}
|
||||
|
||||
export type MonthPickerProps = {
|
||||
currentMonth: Date;
|
||||
onMonthChange: (
|
||||
newMonth: Date | undefined,
|
||||
shouldClosePopover?: boolean
|
||||
) => void;
|
||||
};
|
||||
|
||||
export function MonthPicker({ currentMonth, onMonthChange }: MonthPickerProps) {
|
||||
|
||||
const today = startOfToday();
|
||||
const currentYear = today.getFullYear();
|
||||
const yearFromProp = format(currentMonth, "yyyy");
|
||||
const selectedYear = parseInt(yearFromProp);
|
||||
|
||||
// 현재 월의 시작 날짜
|
||||
const fullYearStart = parse(yearFromProp, "yyyy", new Date());
|
||||
|
||||
const yearEnd =
|
||||
selectedYear === currentYear ? today : endOfYear(fullYearStart);
|
||||
|
||||
|
||||
|
||||
|
||||
const yearRange = Array.from({ length: 21 }, (_, i) => currentYear - 20 + i);
|
||||
const months = eachMonthOfInterval({ start: fullYearStart, end: yearEnd });
|
||||
|
||||
const getMonthButtonClass = (month: Date) =>
|
||||
cn(
|
||||
"inline-flex h-9 w-16 items-center justify-center rounded-md p-0 text-sm font-normal ring-offset-white transition-colors hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 aria-selected:opacity-100 dark:ring-offset-slate-950 dark:hover:bg-slate-800 dark:hover:text-slate-50 dark:focus-visible:ring-slate-800",
|
||||
isEqual(month, currentMonth) &&
|
||||
"bg-slate-900 text-slate-50 hover:bg-slate-900 hover:text-slate-50 focus:bg-slate-900 focus:text-slate-50 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-50 dark:hover:text-slate-900 dark:focus:bg-slate-50 dark:focus:text-slate-900",
|
||||
!isEqual(month, currentMonth) &&
|
||||
isEqual(month, getStartOfCurrentMonth()) &&
|
||||
"bg-slate-100 text-slate-900 dark:bg-slate-800 dark:text-slate-50"
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-3">
|
||||
<div className="flex flex-col space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0">
|
||||
<div className="space-y-4">
|
||||
{/* 년도 셀렉트 */}
|
||||
<div className="flex justify-center pt-1">
|
||||
<Select
|
||||
value={yearFromProp}
|
||||
onValueChange={(selectedYearStr) => {
|
||||
const selectedYearNum = parseInt(selectedYearStr);
|
||||
const newDate = new Date(
|
||||
selectedYearNum,
|
||||
currentMonth.getMonth(),
|
||||
1
|
||||
);
|
||||
onMonthChange(newDate, false);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[120px] h-7">
|
||||
<SelectValue placeholder="년도" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{yearRange.map((year) => (
|
||||
<SelectItem key={year} value={String(year)}>
|
||||
{year}년
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 월 그리드 */}
|
||||
<div
|
||||
className="grid w-full grid-cols-3 gap-2"
|
||||
role="grid"
|
||||
aria-labelledby="month-picker"
|
||||
>
|
||||
{months.map((month) => (
|
||||
<div
|
||||
key={month.toString()}
|
||||
className="relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-slate-100 first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md dark:[&:has([aria-selected])]:bg-slate-800"
|
||||
role="presentation"
|
||||
>
|
||||
<button
|
||||
name="month"
|
||||
className={getMonthButtonClass(month)}
|
||||
disabled={isFuture(month)}
|
||||
role="gridcell"
|
||||
tabIndex={-1}
|
||||
type="button"
|
||||
onClick={() => onMonthChange(month, true)}
|
||||
>
|
||||
<time dateTime={format(month, "yyyy-MM-dd")}>
|
||||
{format(month, 'MMM', { locale: ko })}
|
||||
</time>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from "@common/components";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
type ModalType = {
|
||||
bodyClass: string;
|
||||
@@ -25,15 +26,20 @@ export function UiModal({
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className={bodyClass}>
|
||||
{noHeaderYn === 'N' && (
|
||||
<DialogContent className={bodyClass}>
|
||||
{noHeaderYn === 'N' && (
|
||||
<DialogHeader>
|
||||
<DialogDescription>
|
||||
{headerTitle}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
)}
|
||||
{(open && body)}
|
||||
{(open && body) ?
|
||||
<div> {body}</div>:<></>
|
||||
|
||||
|
||||
|
||||
}
|
||||
{noFooterYn === 'N' && (
|
||||
<DialogFooter className="sm:justify-start">
|
||||
{footerContent}
|
||||
|
||||
@@ -20,4 +20,5 @@ export * from "./table";
|
||||
export * from "./table_div";
|
||||
export * from "./title";
|
||||
export * from "./sdmInsertForm";
|
||||
export * from "./popover_ui"
|
||||
export * from "./popover_ui";
|
||||
export * from "./date_month_picker";
|
||||
@@ -3,9 +3,9 @@ import { SelectBox } from "../select";
|
||||
import { Input } from "@common/components";
|
||||
import { FormUi } from "../form_ui";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { AsYouType } from "libphonenumber-js";
|
||||
import type { ControllerRenderProps, UseFormReturn } from "react-hook-form";
|
||||
import { SdmSearchForm } from "./sdmNameBox";
|
||||
import { set } from "date-fns";
|
||||
|
||||
// 수요처 정보
|
||||
export type DemandCompType = {
|
||||
@@ -79,6 +79,19 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun
|
||||
const [updateCoNm, setUpdateNm] = useState<string >('')
|
||||
|
||||
|
||||
|
||||
const formatPhoneNumber = (value:string) => {
|
||||
const input = value;
|
||||
const formatter = new AsYouType("KR");
|
||||
const formatted = formatter.input(input);
|
||||
|
||||
// 저장할 값 처리: 숫자만 추출
|
||||
const digitsOnly = input.replace(/\D/g, "");
|
||||
|
||||
return formatted
|
||||
};
|
||||
|
||||
|
||||
const initializeSigunData = useCallback((sidoCd: string) => {
|
||||
if (sidoCd === '0') {
|
||||
setSigunValue('0');
|
||||
@@ -325,14 +338,23 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun
|
||||
{
|
||||
name: "telno",
|
||||
label: "전화번호",
|
||||
component: (field) => (
|
||||
component: (field) =>
|
||||
|
||||
{
|
||||
const formattedValue = formatPhoneNumber(field.value??''); // 초기 렌더링 시에도 포맷 적용
|
||||
|
||||
return (
|
||||
<Input
|
||||
placeholder="전화번호를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
{...field}
|
||||
value={formattedValue}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value.replace(/\D/g, ''); // 숫자만 저장
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@@ -41,8 +41,8 @@ const createFormSchema = (isBznmTotNoDisabled: boolean, isDuplicate: boolean ,up
|
||||
telno: z.string()
|
||||
.min(8, "전화번호는 8자 이상 입력해주세요")
|
||||
.max(11, "전화번호는 11자 이내로 입력해주세요")
|
||||
.regex(/^[0-9-]+$/, "전화번호는 숫자와 하이픈(-)만 입력 가능합니다"),
|
||||
rgtr: z.string().optional(),
|
||||
.regex(/^[0-9]+$/, "전화번호는 숫자만 입력 가능합니다"),
|
||||
rgtr: z.string().optional(),
|
||||
dmdAplyPswd : updateList?.dmdInfoPk // 업데이트일경우
|
||||
? z.string().optional() // 패스워드는 선택 사항이며 길이 제한 없음
|
||||
: z.string().optional()
|
||||
|
||||
@@ -301,7 +301,7 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel
|
||||
resetAllValues();
|
||||
// 리셋 후 트리거를 false로 설정
|
||||
if (setRtnPartValue) {
|
||||
setRtnPartValue('0');
|
||||
setRtnPartValue('A');
|
||||
}
|
||||
}
|
||||
}, [resetTrigger]);
|
||||
|
||||
@@ -110,9 +110,7 @@ export function UiTable2({
|
||||
? 'text-right'
|
||||
: head.align === 'center'
|
||||
? 'text-center'
|
||||
: typeof el[head.id] === 'number'
|
||||
? 'text-right'
|
||||
: 'text-center'
|
||||
: 'text-center'
|
||||
}
|
||||
${cellClick && idx === index ? '!font-[1000] cursor-pointer' :
|
||||
cellClick ? 'cursor-pointer' : ''}
|
||||
|
||||
@@ -82,7 +82,7 @@ export function UiTable({ tableData, headlist, pageSize, total, headName, childr
|
||||
boldArray.includes(index)
|
||||
?
|
||||
<TableCell
|
||||
className={`${head.align === 'right' ? 'text-right' : head.align === 'center' ? 'text-center' : typeof el[head.id] === 'number' ? 'text-right' : 'text-center'} ${
|
||||
className={`${head.align === 'right' ? 'text-right' : head.align === 'center' ? 'text-center' : 'text-center'} ${
|
||||
cellClick && idx === index ? '!font-[1000] cursor-pointer' :
|
||||
cellClick ? 'cursor-pointer' : ''
|
||||
} !font-bold`}
|
||||
@@ -97,7 +97,7 @@ export function UiTable({ tableData, headlist, pageSize, total, headName, childr
|
||||
</TableCell>
|
||||
:
|
||||
<TableCell
|
||||
className={`${head.align === 'right' ? 'text-right' : head.align === 'center' ? 'text-center' : typeof el[head.id] === 'number' ? 'text-right' : 'text-center'} ${
|
||||
className={`${head.align === 'right' ? 'text-right' : head.align === 'center' ? 'text-center' : 'text-center'} ${
|
||||
cellClick && idx === index ? '!font-[1000] cursor-pointer' :
|
||||
cellClick ? 'cursor-pointer' : ''
|
||||
}`}
|
||||
|
||||
Reference in New Issue
Block a user